January 3rd, 2025

Goal

Bugs Fixed.

Hypothesis

If the bugs are resolved, users will be able to operate the program without experiencing crashes.

Expected Results

Results

Bugs Fixed: Special thanks to David for stress-testing the program last week. The following issues have been resolved:

Next Steps

My next step is to address any bugs that David may encounter during testing. I’ll focus on stabilizing the modules with outstanding issues and create documentation that explains the current status of the codebase and protocol for packaging.

Files changed (57) hide show
  1. README.md +75 -0
  2. requirements.txt +18 -0
  3. scripts/mac.spec +0 -69
  4. src/controllers/FindTargetsController.py +84 -38
  5. src/controllers/GenerateLibraryController.py +26 -15
  6. src/controllers/HomeWindowController.py +314 -202
  7. src/controllers/MainWindowController copy.py +0 -262
  8. src/controllers/MainWindowController.py +284 -78
  9. src/controllers/NCBIWindowController.py +14 -1
  10. src/controllers/NewGenomeWindowController.py +111 -60
  11. src/controllers/OffTargetController.py +12 -0
  12. src/controllers/PopulationAnalysisWindowController.py +0 -6
  13. src/controllers/StartupWindowController.py +67 -9
  14. src/controllers/ViewTargetsController.py +265 -194
  15. src/main.py +36 -6
  16. src/models/AnnotationParser.py +63 -28
  17. src/models/BaseModel.py +58 -0
  18. src/models/ConfigManager.py +57 -10
  19. src/models/DatabaseManager.py +283 -78
  20. src/models/FindTargetsModel.py +21 -18
  21. src/models/GenerateLibraryModel.py +174 -49
  22. src/models/GlobalSettings.py +172 -33
  23. src/models/HomeWindowModel.py +23 -22
  24. src/models/NCBIWindowModel.py +23 -4
  25. src/models/NewGenomeWindowModel.py +2 -2
  26. src/models/OffTarget/local_output.txt +1 -29
  27. src/models/OffTargetModel.py +18 -23
  28. src/models/PopulationAnalysisWindowModel.py +0 -5
  29. src/models/StartupWindowModel.py +24 -7
  30. src/models/ViewTargetsModel.py +104 -124
  31. src/ui/find_targets.ui +0 -25
  32. src/ui/home_window.ui +45 -108
  33. src/ui/main_window.ui +1 -26
  34. src/ui/new_genome_window.ui +0 -34
  35. src/ui/view_targets.ui +89 -64
  36. src/views/CloseableTabWidget.py +43 -11
  37. src/views/FindTargetsView.py +44 -18
  38. src/views/GenerateLibraryView.py +24 -5
  39. src/views/HomeWindowView.py +67 -38
  40. src/views/LoadingDialog.py +9 -34
  41. src/views/MultitargetingWindowView.py +10 -0
  42. src/views/NCBIWindowView.py +38 -0
  43. src/views/NewEndonucleaseView.py +16 -13
  44. src/views/NewGenomeWindowView.py +10 -16
  45. src/views/PopulationAnalysisWindowView.py +12 -25
  46. src/views/StartupWindowView.py +2 -1
  47. src/views/ViewTargetsView.py +334 -89
  48. src/views/dialogs/base_insertion_dialog.py +56 -0
  49. src/views/dna_viewer/__init__.py +1 -0
  50. src/views/dna_viewer/components/__init__.py +1 -0
  51. src/views/dna_viewer/components/feature_viewer.py +152 -0
  52. src/views/dna_viewer/components/nucleotide_item.py +108 -0
  53. src/views/dna_viewer/components/ruler.py +121 -0
  54. src/views/dna_viewer/components/sequence_cursor.py +61 -0
  55. src/views/dna_viewer/components/sequence_insertion_zone.py +356 -0
  56. src/views/dna_viewer/components/sequence_viewer.py +438 -0
  57. src/views/dna_viewer/dna_feature_viewer.py +309 -0
README.md CHANGED
@@ -20,3 +20,78 @@ Thank you for your interest in CASPER. Our packaged releases for Windows 10 and
20
  CASPER will launch and you will be good to go! If you have any problems, please email David Dooley at ddooley2@vols.utk.edu
21
 
22
  NOTE: CASPER may take a long time to launch for the first time due to initialization in the background. After the first launch, it should load much faster.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  CASPER will launch and you will be good to go! If you have any problems, please email David Dooley at ddooley2@vols.utk.edu
21
 
22
  NOTE: CASPER may take a long time to launch for the first time due to initialization in the background. After the first launch, it should load much faster.
23
+
24
+ ### Docker Installation (macOS)
25
+
26
+ #### Prerequisites
27
+ 1. Install Docker Desktop for Mac
28
+ ```bash
29
+ brew install --cask docker
30
+ ```
31
+
32
+ 2. Install XQuartz
33
+ ```bash
34
+ brew install --cask xquartz
35
+ ```
36
+
37
+ 3. Configure XQuartz:
38
+ ```bash
39
+ # Start XQuartz
40
+ open -a XQuartz
41
+
42
+ # In XQuartz Preferences → Security:
43
+ # - Check "Allow connections from network clients"
44
+ # - Restart XQuartz after changing settings
45
+ ```
46
+
47
+ 4. Set up X11 forwarding (run these commands each time before starting the app):
48
+ ```bash
49
+ # Start XQuartz if not running
50
+ open -a XQuartz
51
+
52
+ # Get your IP address
53
+ export IP=$(ifconfig en0 | grep inet | awk '$1=="inet" {print $2}')
54
+
55
+ # Set up permissions (use your actual IP)
56
+ xhost + $IP
57
+
58
+ # Clean up any old containers
59
+ docker-compose down
60
+ ```
61
+
62
+ 5. Run CASPER:
63
+ ```bash
64
+ # First time or after making changes:
65
+ docker-compose up --build
66
+
67
+ # Subsequent runs (without rebuilding):
68
+ docker-compose up
69
+
70
+ # Or run in background:
71
+ docker-compose up -d
72
+ ```
73
+
74
+ #### Troubleshooting
75
+ - If the app doesn't start:
76
+ ```bash
77
+ # Stop all containers
78
+ docker-compose down
79
+
80
+ # Remove old containers and images
81
+ docker system prune -f
82
+
83
+ # Restart XQuartz
84
+ killall Xquartz
85
+ open -a XQuartz
86
+
87
+ # Set up X11 again
88
+ xhost + localhost
89
+
90
+ # Try running again
91
+ docker-compose up --build
92
+ ```
93
+
94
+ - If you still have issues:
95
+ - Make sure Docker Desktop is running
96
+ - Try restarting your computer
97
+ - Run `docker-compose logs` to see detailed error messages
requirements.txt CHANGED
@@ -1,30 +1,48 @@
 
 
1
  beautifulsoup4==4.12.3
2
  biopython==1.84
 
 
3
  contourpy==1.3.0
4
  cycler==0.12.1
5
  darkdetect==0.7.1
 
6
  fonttools==4.53.1
 
 
 
7
  joblib==1.4.2
8
  kiwisolver==1.4.7
9
  lxml==5.3.0
 
10
  matplotlib==3.9.2
 
11
  mplcursors==0.5.3
12
  numpy==2.1.1
13
  packaging==24.1
14
  pandas==2.2.2
15
  pillow==10.4.0
 
 
 
 
16
  pyparsing==3.1.4
17
  PyQt6==6.7.1
18
  PyQt6-Qt6==6.7.2
19
  PyQt6_sip==13.8.0
20
  pyqtdarktheme==2.1.0
 
21
  python-dateutil==2.9.0.post0
22
  python-dotenv==1.0.1
23
  pytz==2024.1
24
  PyYAML==6.0.2
 
25
  scikit-learn==1.5.2
26
  scipy==1.14.1
27
  six==1.16.0
28
  soupsieve==2.6
29
  threadpoolctl==3.5.0
 
30
  tzdata==2024.1
 
 
1
+ altgraph==0.17.4
2
+ astroid==3.3.5
3
  beautifulsoup4==4.12.3
4
  biopython==1.84
5
+ certifi==2024.8.30
6
+ charset-normalizer==3.4.0
7
  contourpy==1.3.0
8
  cycler==0.12.1
9
  darkdetect==0.7.1
10
+ dill==0.3.9
11
  fonttools==4.53.1
12
+ graphviz==0.20.3
13
+ idna==3.10
14
+ isort==5.13.2
15
  joblib==1.4.2
16
  kiwisolver==1.4.7
17
  lxml==5.3.0
18
+ macholib==1.16.3
19
  matplotlib==3.9.2
20
+ mccabe==0.7.0
21
  mplcursors==0.5.3
22
  numpy==2.1.1
23
  packaging==24.1
24
  pandas==2.2.2
25
  pillow==10.4.0
26
+ platformdirs==4.3.6
27
+ pyinstaller==6.11.1
28
+ pyinstaller-hooks-contrib==2024.10
29
+ pylint==3.3.1
30
  pyparsing==3.1.4
31
  PyQt6==6.7.1
32
  PyQt6-Qt6==6.7.2
33
  PyQt6_sip==13.8.0
34
  pyqtdarktheme==2.1.0
35
+ python-call-graph==2.1.2
36
  python-dateutil==2.9.0.post0
37
  python-dotenv==1.0.1
38
  pytz==2024.1
39
  PyYAML==6.0.2
40
+ requests==2.32.3
41
  scikit-learn==1.5.2
42
  scipy==1.14.1
43
  six==1.16.0
44
  soupsieve==2.6
45
  threadpoolctl==3.5.0
46
+ tomlkit==0.13.2
47
  tzdata==2024.1
48
+ urllib3==2.2.3
scripts/mac.spec DELETED
@@ -1,69 +0,0 @@
1
- block_cipher = None
2
-
3
- a = Analysis(['src/main.py'],
4
- pathex=['src'],
5
- datas=[
6
- ('assets', 'assets'),
7
- ('config', 'config'),
8
- ('logs', 'logs'),
9
- ('src', 'src'),
10
- ('genomeBrowserTemplate.html', '.'),
11
- ],
12
- hiddenimports=[],
13
- hookspath=[],
14
- runtime_hooks=[],
15
- excludes=[],
16
- win_no_prefer_redirects=False,
17
- win_private_assemblies=False,
18
- cipher=block_cipher,
19
- noarchive=False)
20
-
21
- pyz = PYZ(a.pure, a.zipped_data,
22
- cipher=block_cipher)
23
-
24
- exe = EXE(pyz,
25
- a.scripts,
26
- [],
27
- exclude_binaries=True,
28
- name='CASPERapp',
29
- debug=False,
30
- bootloader_ignore_signals=False,
31
- strip=False,
32
- upx=True,
33
- console=False,
34
- disable_windowed_traceback=False,
35
- target_arch=None,
36
- codesign_identity=None,
37
- entitlements_file=None,
38
- icon='assets/CASPER_icon.icns')
39
-
40
- coll = COLLECT(exe,
41
- a.binaries,
42
- a.zipfiles,
43
- a.datas,
44
- strip=False,
45
- upx=True,
46
- upx_exclude=[],
47
- name='CASPERapp')
48
-
49
- app = BUNDLE(coll,
50
- name='CASPERapp.app',
51
- icon='assets/CASPER_icon.icns',
52
- version='2.0.1',
53
- bundle_identifier=None)
54
-
55
- # 1. Have the mac.spec in the app directory
56
- # 2. pyinstaller mac.spec
57
- # 3. mkdir -p dist/dmg
58
- # 4. rm -r dist/dmg/*
59
- # 5. Manual copy of the app into dist/dmg
60
- # 6. create-dmg \
61
- # --volname "CASPERapp" \
62
- # --window-pos 200 120 \
63
- # --window-size 600 300 \
64
- # --icon-size 100 \
65
- # --icon "CASPERapp.app" 175 120 \
66
- # --hide-extension "CASPERapp.app" \
67
- # --app-drop-link 425 120 \
68
- # "dist/CASPERapp.dmg" \
69
- # "dist/dmg/"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/controllers/FindTargetsController.py CHANGED
@@ -4,6 +4,7 @@ from views.FindTargetsView import FindTargetsView
4
  from PyQt6.QtWidgets import QMessageBox
5
  from views.LoadingDialog import LoadingDialog
6
  from PyQt6.QtWidgets import QApplication
 
7
 
8
  class FindTargetsController:
9
  def __init__(self, global_settings):
@@ -26,17 +27,28 @@ class FindTargetsController:
26
  self.global_settings.logger.debug(f"FindTargetsController received new annotation file: {new_annotation_file}")
27
  self._current_annotation_file = new_annotation_file
28
 
29
- # Clear the current results
30
- if self.view and hasattr(self.view, 'results_table'):
31
- self.view.clear_results()
 
 
 
 
 
 
 
32
 
33
- # If we have previous input data, rerun the search with the new annotation file
34
- if self._input_data:
 
 
 
35
  self._input_data['annotation_file'] = new_annotation_file
36
  self._process_input_data(self._input_data)
37
 
38
  except Exception as e:
39
  self.global_settings.logger.error(f"Error handling annotation file change: {str(e)}")
 
40
 
41
  def _connect_signals(self):
42
  """Connect view signals"""
@@ -93,22 +105,35 @@ class FindTargetsController:
93
  QMessageBox.warning(self.view, "No Selection", "Please select targets to view.")
94
  return
95
 
96
- # Create loading dialog
97
- loading_dialog = LoadingDialog(self.view)
 
98
  loading_dialog.show()
99
  loading_dialog.set_progress(0)
100
  QApplication.processEvents()
101
 
102
  try:
103
- # Find existing View Targets tab
104
- main_window = self.global_settings.main_window
105
- existing_tab = main_window.find_tab_by_title("View Targets")
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  loading_dialog.set_message("Initializing view targets...", 25)
108
  QApplication.processEvents()
109
 
110
  if existing_tab:
111
- view_targets_controller = main_window.tab_widgets['controllers'].get("View Targets")
112
  if view_targets_controller:
113
  loading_dialog.set_message("Loading guides...", 50)
114
  QApplication.processEvents()
@@ -124,7 +149,7 @@ class FindTargetsController:
124
  # Switch to the existing tab
125
  main_window.view.tab_widget.setCurrentWidget(existing_tab)
126
  else:
127
- self.logger.error("View Targets controller not found for existing tab")
128
  else:
129
  loading_dialog.set_message("Creating view targets...", 25)
130
  QApplication.processEvents()
@@ -139,7 +164,7 @@ class FindTargetsController:
139
  loading_dialog=loading_dialog
140
  )
141
 
142
- main_window.open_new_tab("View Targets", view_targets_controller)
143
 
144
  finally:
145
  loading_dialog.close()
@@ -168,34 +193,55 @@ class FindTargetsController:
168
  def open_view_targets_directly(self, input_data):
169
  """Open view targets directly for position-based searches"""
170
  try:
171
- # Get targets using the model
172
- targets = self.model.find_targets_by_position(
173
- self.model._get_parser(self.model.get_cspr_file_path(input_data)),
174
- input_data
175
- )
176
-
177
- if targets:
178
- # Create view targets controller
179
- view_targets_controller = self.global_settings.get_view_targets_window()
 
180
 
181
- # Load targets directly
182
- view_targets_controller.load_targets(
183
- targets,
184
- input_data['organism'],
185
- input_data['endonuclease']
186
  )
187
 
188
- # Open view targets tab
189
- self.global_settings.main_window.open_new_tab(
190
- "View Targets",
191
- view_targets_controller
192
- )
193
- else:
194
- QMessageBox.warning(
195
- self.view,
196
- "No Targets Found",
197
- "No targets were found in the specified position range."
198
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
  except Exception as e:
201
  self.global_settings.logger.error(f"Error opening view targets directly: {str(e)}")
 
4
  from PyQt6.QtWidgets import QMessageBox
5
  from views.LoadingDialog import LoadingDialog
6
  from PyQt6.QtWidgets import QApplication
7
+ import os
8
 
9
  class FindTargetsController:
10
  def __init__(self, global_settings):
 
27
  self.global_settings.logger.debug(f"FindTargetsController received new annotation file: {new_annotation_file}")
28
  self._current_annotation_file = new_annotation_file
29
 
30
+ # Only process if we have a valid annotation file and input data
31
+ if new_annotation_file and self._input_data:
32
+ # Verify annotation file exists
33
+ annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', new_annotation_file)
34
+ if not os.path.isfile(annotation_path):
35
+ # Try without GBFF subdirectory
36
+ annotation_path = os.path.join(self.global_settings.get_db_path(), new_annotation_file)
37
+ if not os.path.isfile(annotation_path):
38
+ self.logger.warning(f"Annotation file not found at {annotation_path}")
39
+ return
40
 
41
+ # Clear the current results
42
+ if self.view and hasattr(self.view, 'results_table'):
43
+ self.view.clear_results()
44
+
45
+ # Update input data with new annotation file
46
  self._input_data['annotation_file'] = new_annotation_file
47
  self._process_input_data(self._input_data)
48
 
49
  except Exception as e:
50
  self.global_settings.logger.error(f"Error handling annotation file change: {str(e)}")
51
+ # Don't raise the error since this is an event handler
52
 
53
  def _connect_signals(self):
54
  """Connect view signals"""
 
105
  QMessageBox.warning(self.view, "No Selection", "Please select targets to view.")
106
  return
107
 
108
+ # Create loading dialog with the main window as parent
109
+ main_window = self.global_settings.main_window
110
+ loading_dialog = LoadingDialog(main_window.view)
111
  loading_dialog.show()
112
  loading_dialog.set_progress(0)
113
  QApplication.processEvents()
114
 
115
  try:
116
+ # Get the current Find Targets tab number
117
+ current_tab_index = main_window.view.tab_widget.currentIndex()
118
+ current_tab_title = main_window.view.tab_widget.tabText(current_tab_index)
119
+
120
+ # Extract number from Find Targets tab (if any)
121
+ view_targets_title = "View Targets"
122
+ if current_tab_title != "Find Targets":
123
+ try:
124
+ number = current_tab_title.split()[-1]
125
+ view_targets_title = f"View Targets {number}"
126
+ except (IndexError, ValueError):
127
+ pass
128
+
129
+ # Find existing View Targets tab with the same number
130
+ existing_tab = main_window.find_tab_by_title(view_targets_title)
131
 
132
  loading_dialog.set_message("Initializing view targets...", 25)
133
  QApplication.processEvents()
134
 
135
  if existing_tab:
136
+ view_targets_controller = main_window.tab_widgets['controllers'].get(view_targets_title)
137
  if view_targets_controller:
138
  loading_dialog.set_message("Loading guides...", 50)
139
  QApplication.processEvents()
 
149
  # Switch to the existing tab
150
  main_window.view.tab_widget.setCurrentWidget(existing_tab)
151
  else:
152
+ self.logger.error(f"View Targets controller not found for existing tab {view_targets_title}")
153
  else:
154
  loading_dialog.set_message("Creating view targets...", 25)
155
  QApplication.processEvents()
 
164
  loading_dialog=loading_dialog
165
  )
166
 
167
+ main_window.open_new_tab(view_targets_title, view_targets_controller)
168
 
169
  finally:
170
  loading_dialog.close()
 
193
  def open_view_targets_directly(self, input_data):
194
  """Open view targets directly for position-based searches"""
195
  try:
196
+ # Create loading dialog with the main window as parent
197
+ main_window = self.global_settings.main_window
198
+ loading_dialog = LoadingDialog(main_window.view)
199
+ loading_dialog.show()
200
+ loading_dialog.set_progress(0)
201
+ QApplication.processEvents()
202
+
203
+ try:
204
+ loading_dialog.set_message("Finding targets...", 25)
205
+ QApplication.processEvents()
206
 
207
+ # Get targets using the model
208
+ targets = self.model.find_targets_by_position(
209
+ self.model._get_parser(self.model.get_cspr_file_path(input_data)),
210
+ input_data
 
211
  )
212
 
213
+ if targets:
214
+ loading_dialog.set_message("Creating view targets...", 50)
215
+ QApplication.processEvents()
216
+
217
+ # Create view targets controller
218
+ view_targets_controller = self.global_settings.get_view_targets_window()
219
+
220
+ loading_dialog.set_message("Loading guides...", 75)
221
+ QApplication.processEvents()
222
+
223
+ # Load targets directly
224
+ view_targets_controller.load_targets(
225
+ targets,
226
+ input_data['organism'],
227
+ input_data['endonuclease']
228
+ )
229
+
230
+ # Open view targets tab
231
+ self.global_settings.main_window.open_new_tab(
232
+ "View Targets",
233
+ view_targets_controller
234
+ )
235
+ else:
236
+ QMessageBox.warning(
237
+ self.view,
238
+ "No Targets Found",
239
+ "No targets were found in the specified position range."
240
+ )
241
+
242
+ finally:
243
+ loading_dialog.close()
244
+ QApplication.processEvents()
245
 
246
  except Exception as e:
247
  self.global_settings.logger.error(f"Error opening view targets directly: {str(e)}")
src/controllers/GenerateLibraryController.py CHANGED
@@ -13,10 +13,18 @@ class GenerateLibraryController(QObject):
13
  self.model = GenerateLibraryModel(global_settings)
14
  self.view = GenerateLibraryView(global_settings)
15
 
 
 
 
 
 
 
 
 
16
  # Get CSPR file path
17
- if selected_targets and len(selected_targets) > 0:
18
  # Get organism name from the first target's chromosome
19
- chrom = selected_targets[0].get('full_chromosome', '')
20
  if chrom:
21
  # Extract organism name from chromosome ID
22
  org_name = chrom.split('.')[0]
@@ -25,7 +33,7 @@ class GenerateLibraryController(QObject):
25
 
26
  # Get CSPR file path and initialize parser
27
  org_files = self.model.get_organism_to_files()
28
- endonuclease = selected_targets[0].get('endonuclease', '').lower()
29
  if org_name in org_files and endonuclease in org_files[org_name]:
30
  cspr_file = os.path.join(
31
  self.global_settings.get_db_path(),
@@ -35,7 +43,7 @@ class GenerateLibraryController(QObject):
35
 
36
  # Get guide data for each target
37
  processed_targets = []
38
- for target in selected_targets:
39
  target_info = [{
40
  'feature_id': target['feature_id'],
41
  'feature_name': target['feature_name'],
@@ -63,11 +71,6 @@ class GenerateLibraryController(QObject):
63
  self.selected_targets = processed_targets
64
  else:
65
  self.selected_targets = selected_targets
66
- else:
67
- self.selected_targets = selected_targets
68
-
69
- # Log initialization
70
- self.logger.debug(f"Initializing GenerateLibraryController with {len(selected_targets) if selected_targets else 0} targets")
71
 
72
  self._connect_signals()
73
 
@@ -75,15 +78,12 @@ class GenerateLibraryController(QObject):
75
  """Connect view signals to controller methods"""
76
  try:
77
  self.view.submit_clicked.connect(self._handle_submit)
78
- self.logger.debug("Connected GenerateLibraryView signals")
79
  except Exception as e:
80
  self.logger.error(f"Error connecting signals: {str(e)}")
81
 
82
  def show(self):
83
  """Show the generate library window"""
84
  try:
85
- self.logger.debug("Showing GenerateLibraryView")
86
-
87
  # Store reference to prevent garbage collection
88
  self.global_settings.main_window._current_generate_library_controller = self
89
 
@@ -184,13 +184,17 @@ class GenerateLibraryController(QObject):
184
  else:
185
  raise ValueError(f"Could not find organism {org_name} in database")
186
 
 
 
 
 
187
  # Generate library using processed targets
188
  success = self.model.generate_library(
189
  self.processed_targets if hasattr(self, 'processed_targets') else self.selected_targets,
190
  settings
191
  )
192
 
193
- if success:
194
  self.view.show_success("Library generated successfully!")
195
  self.view.close()
196
 
@@ -221,9 +225,16 @@ class GenerateLibraryController(QObject):
221
 
222
  if settings.get('find_off_targets'):
223
  max_score = settings.get('max_off_target_score')
224
- if max_score is None or not 0 < max_score <= 0.5:
225
- raise ValueError("Maximum off-target score must be between 0 and 0.5")
226
 
227
  except Exception as e:
228
  self.logger.error(f"Settings validation error: {str(e)}")
229
  raise
 
 
 
 
 
 
 
 
13
  self.model = GenerateLibraryModel(global_settings)
14
  self.view = GenerateLibraryView(global_settings)
15
 
16
+ # Get selected targets from global settings if not provided
17
+ if selected_targets is None and hasattr(self.global_settings, '_current_selected_targets'):
18
+ selected_targets = self.global_settings._current_selected_targets
19
+
20
+ self.selected_targets = selected_targets or []
21
+
22
+ self.view.ledFileName.setText('eck_12_spCas9_lib')
23
+
24
  # Get CSPR file path
25
+ if self.selected_targets and len(self.selected_targets) > 0:
26
  # Get organism name from the first target's chromosome
27
+ chrom = self.selected_targets[0].get('full_chromosome', '')
28
  if chrom:
29
  # Extract organism name from chromosome ID
30
  org_name = chrom.split('.')[0]
 
33
 
34
  # Get CSPR file path and initialize parser
35
  org_files = self.model.get_organism_to_files()
36
+ endonuclease = self.selected_targets[0].get('endonuclease', '').lower()
37
  if org_name in org_files and endonuclease in org_files[org_name]:
38
  cspr_file = os.path.join(
39
  self.global_settings.get_db_path(),
 
43
 
44
  # Get guide data for each target
45
  processed_targets = []
46
+ for target in self.selected_targets:
47
  target_info = [{
48
  'feature_id': target['feature_id'],
49
  'feature_name': target['feature_name'],
 
71
  self.selected_targets = processed_targets
72
  else:
73
  self.selected_targets = selected_targets
 
 
 
 
 
74
 
75
  self._connect_signals()
76
 
 
78
  """Connect view signals to controller methods"""
79
  try:
80
  self.view.submit_clicked.connect(self._handle_submit)
 
81
  except Exception as e:
82
  self.logger.error(f"Error connecting signals: {str(e)}")
83
 
84
  def show(self):
85
  """Show the generate library window"""
86
  try:
 
 
87
  # Store reference to prevent garbage collection
88
  self.global_settings.main_window._current_generate_library_controller = self
89
 
 
184
  else:
185
  raise ValueError(f"Could not find organism {org_name} in database")
186
 
187
+ # Connect to model's progress signal if off-target analysis is enabled
188
+ if settings.get('find_off_targets'):
189
+ self.model.progress_updated.connect(self._handle_progress)
190
+
191
  # Generate library using processed targets
192
  success = self.model.generate_library(
193
  self.processed_targets if hasattr(self, 'processed_targets') else self.selected_targets,
194
  settings
195
  )
196
 
197
+ if success and not settings.get('find_off_targets'):
198
  self.view.show_success("Library generated successfully!")
199
  self.view.close()
200
 
 
225
 
226
  if settings.get('find_off_targets'):
227
  max_score = settings.get('max_off_target_score')
228
+ if max_score is None or not 0 <= max_score <= 0.5:
229
+ raise ValueError("Maximum off-target score must be between 0 and 0.5 inclusive")
230
 
231
  except Exception as e:
232
  self.logger.error(f"Settings validation error: {str(e)}")
233
  raise
234
+
235
+ def _handle_progress(self, value):
236
+ """Handle progress updates from model"""
237
+ try:
238
+ self.view.progBar.setValue(value)
239
+ except Exception as e:
240
+ self.logger.error(f"Error updating progress: {str(e)}")
src/controllers/HomeWindowController.py CHANGED
@@ -7,69 +7,121 @@ from models.DatabaseManager import FileChangeType
7
  import time
8
  from views.LoadingDialog import LoadingDialog
9
  from PyQt6.QtWidgets import QApplication
 
10
 
11
  class HomeWindowController:
12
  def __init__(self, global_settings):
13
- self.global_settings = global_settings
14
- self.logger = global_settings.get_logger()
 
 
15
  try:
16
- self.model = HomeWindowModel(global_settings)
17
- self.view = HomeWindowView(global_settings)
18
- self.init_ui()
19
- self.setup_connections()
20
- self.model.load_data()
21
- self.global_settings.db_manager.db_files_changed.connect(self._handle_db_files_changed)
22
- self.global_settings.db_manager.db_validation_changed.connect(self._handle_db_validation_changed)
23
- self.global_settings.db_manager.db_state_changed.connect(self._handle_db_state_changed)
24
  except Exception as e:
25
- show_error(self.global_settings, "Error initializing HomeWindowController", str(e))
26
 
27
- def init_ui(self):
 
28
  try:
29
- self.load_combo_box_data()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  self.handle_search_type_change()
31
  except Exception as e:
32
- show_error(self.global_settings, "Error initializing UI in HomeWindowController", str(e))
33
 
34
- def load_combo_box_data(self):
35
- """Reload all combo box data"""
36
  try:
37
- self.model.load_data()
38
-
39
- organism_to_endonuclease = self.model.get_organism_to_endonuclease()
40
  annotation_files = self.model.get_annotation_files()
41
 
42
- # Update organisms combo box
43
- self.view.update_combo_box_organism(sorted(organism_to_endonuclease.keys()))
 
 
 
 
 
 
 
 
 
44
 
45
- # Update endonuclease combo box
46
- self.update_combo_box_endonuclease()
 
 
 
 
 
 
47
 
48
- # Update annotation files combo box
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  self.view.update_combo_box_annotation_files(annotation_files)
50
-
51
  except Exception as e:
52
- show_error(self.global_settings, "Error loading dropdown data", str(e))
53
 
54
- def update_combo_box_endonuclease(self):
55
- selected_organism = self.view.combo_box_organism.currentText()
56
- endonuclease = self.model.get_organism_to_endonuclease().get(selected_organism, [])
57
- self.logger.debug(f"Updating endonuclease combo box for organism {selected_organism} with endonuclease: {endonuclease} in Main window")
58
- self.view.update_combo_box_endonuclease(endonuclease)
59
 
60
- def setup_connections(self):
61
  try:
62
  # grpNavigationMenu
63
- self.view.push_button_new_genome.clicked.connect(self.open_new_genome_module)
64
- self.view.push_button_new_endonuclease.clicked.connect(self.open_new_endonuclease_module)
65
- self.view.push_button_multitargeting_analysis.clicked.connect(self.open_multitargeting_analysis_module)
66
- self.view.push_button_population_analysis.clicked.connect(self.open_population_analysis_module)
67
 
68
  # grpStep1
69
- self.view.combo_box_organism.currentIndexChanged.connect(self.update_combo_box_endonuclease)
70
 
71
  # grpStep2
72
- self.view.push_button_ncbi_file_search.clicked.connect(self.open_ncbi_window)
73
 
74
  # grpStep3
75
  self.view.radio_button_feature.clicked.connect(self.handle_search_type_change)
@@ -79,40 +131,122 @@ class HomeWindowController:
79
 
80
  # Add connection for annotation file changes
81
  self.view.combo_box_local_annotation_files.currentTextChanged.connect(self._on_annotation_file_changed)
 
 
 
 
 
 
82
  except Exception as e:
83
- show_error(self.global_settings, "Error setting up connections in HomeWindowController", str(e))
84
-
85
 
86
- # Event Handlers
87
- def gather_settings(self):
88
- """Process input data and direct to appropriate view"""
89
  try:
90
- input_data = self.view.get_find_targets_input()
 
91
 
92
- if input_data['search_type'] == 'sequence':
93
- sequence = input_data['search_query'].strip()
94
- if len(sequence) < 100:
95
- QMessageBox.warning(
96
- self.view,
97
- "Sequence Too Short",
98
- "The sequence given is too small. At least 100 characters are required."
99
- )
100
- return
101
- if len(sequence) > 10000:
102
- QMessageBox.warning(
103
- self.view,
104
- "Sequence Too Long",
105
- "The sequence given is too large. Maximum allowed length is 10,000 base pairs."
106
- )
107
- return
108
- self.open_view_targets(input_data)
109
- elif input_data['search_type'] == 'position':
110
- self.open_view_targets(input_data)
111
- else:
112
- self.open_find_targets_module()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  except Exception as e:
115
- show_error(self.global_settings, "Error in gather_settings", str(e))
116
 
117
  def open_view_targets(self, input_data):
118
  try:
@@ -124,7 +258,7 @@ class HomeWindowController:
124
 
125
  try:
126
  # Create find targets controller to use its model
127
- find_targets_controller = self.global_settings.get_find_targets_window()
128
 
129
  # For position searches, handle each query separately
130
  if input_data['search_type'] == 'position':
@@ -164,7 +298,7 @@ class HomeWindowController:
164
  QApplication.processEvents()
165
 
166
  # Close existing View Targets tab if it exists
167
- main_window = self.global_settings.main_window
168
  existing_tab = main_window.find_tab_by_title("View Targets")
169
  if existing_tab:
170
  tab_index = main_window.view.tab_widget.indexOf(existing_tab)
@@ -174,7 +308,7 @@ class HomeWindowController:
174
  # Create view targets controller
175
  loading_dialog.set_message("Creating view targets...", 90)
176
  QApplication.processEvents()
177
- view_targets_controller = self.global_settings.get_view_targets_window()
178
 
179
  view_targets_controller.load_guides(
180
  targets,
@@ -200,10 +334,10 @@ class HomeWindowController:
200
  loading_dialog.close()
201
 
202
  except Exception as e:
203
- self.global_settings.logger.error(f"Error opening view targets directly: {str(e)}")
204
- show_error(self.global_settings, "Error", f"Could not open view targets: {str(e)}")
205
 
206
- def open_find_targets_module(self):
207
  """Open find targets module for non-position searches"""
208
  try:
209
  # Show loading dialog
@@ -213,71 +347,112 @@ class HomeWindowController:
213
  QApplication.processEvents()
214
 
215
  try:
216
- # Close existing Find Targets tab if it exists
217
- main_window = self.global_settings.main_window
218
- existing_tab = main_window.find_tab_by_title("Find Targets")
219
- if existing_tab:
220
- tab_index = main_window.view.tab_widget.indexOf(existing_tab)
221
- main_window._close_tab(tab_index)
222
- self.logger.debug("Closed existing Find Targets tab")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
  loading_dialog.set_progress(40)
225
 
226
  # Create new find targets controller and load data
227
- find_targets_controller = self.global_settings.get_find_targets_window()
228
  input_data = self.view.get_find_targets_input()
229
  loading_dialog.set_progress(60)
230
 
231
  find_targets_controller.find_targets(input_data)
232
  loading_dialog.set_progress(80)
233
 
234
- # Open new Find Targets tab
235
- self.global_settings.main_window.open_new_tab("Find Targets", find_targets_controller)
 
236
  loading_dialog.set_progress(100)
237
 
238
  finally:
239
  loading_dialog.close()
240
 
241
  except Exception as e:
242
- show_error(self.global_settings, "Error in open_find_targets_module() in Home", str(e))
243
 
244
- def toggle_annotation(self):
245
- # Implementation for toggling annotation
246
- pass
247
-
248
- def open_new_genome_module(self):
249
  try:
250
- main_window = self.global_settings.main_window
251
  existing_tab = main_window.find_tab_by_title("New Genome")
252
 
253
  if existing_tab:
254
  main_window.view.tab_widget.setCurrentWidget(existing_tab)
255
  main_window._resize_for_tab("New Genome")
256
  else:
257
- new_genome_controller = self.global_settings.get_new_genome_window()
258
  main_window.open_new_tab("New Genome", new_genome_controller)
259
  except Exception as e:
260
- show_error(self.global_settings, "Error in open_new_genome_widget() in Home", str(e))
261
 
262
- def open_new_endonuclease_module(self):
263
  try:
264
- main_window = self.global_settings.main_window
265
- existing_tab = main_window.find_tab_by_title("Define New Endonuclease")
266
- if existing_tab:
267
- main_window.view.tab_widget.setCurrentWidget(existing_tab)
268
- main_window._resize_for_tab("Define New Endonuclease")
269
- else:
270
- new_endonuclease_controller = self.global_settings.get_new_endonuclease_window()
271
- main_window.open_new_tab("Define New Endonuclease", new_endonuclease_controller)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  except Exception as e:
273
- show_error(self.global_settings, "Error in open_new_endonuclease_widget() in main", str(e))
274
 
275
- def open_multitargeting_analysis_module(self):
276
  try:
277
  start_time = time.time()
278
  self.logger.debug("Starting multitargeting analysis module launch")
279
 
280
- main_window = self.global_settings.main_window
281
  existing_tab = main_window.find_tab_by_title("Multitargeting Analysis")
282
 
283
  tab_check_time = time.time()
@@ -289,7 +464,7 @@ class HomeWindowController:
289
  self.logger.debug(f"Switched to existing tab: {time.time() - tab_check_time:.2f} seconds")
290
  else:
291
  controller_start = time.time()
292
- multitargeting_controller = self.global_settings.get_multitargeting_window()
293
  self.logger.debug(f"Controller creation took: {time.time() - controller_start:.2f} seconds")
294
 
295
  tab_open_start = time.time()
@@ -298,119 +473,56 @@ class HomeWindowController:
298
 
299
  self.logger.debug(f"Total multitargeting module launch took: {time.time() - start_time:.2f} seconds")
300
  except Exception as e:
301
- show_error(self.global_settings, "Error in open_multitargeting_analysis_widget() in Home", str(e))
302
 
303
- def open_population_analysis_module(self):
304
  try:
305
- main_window = self.global_settings.main_window
306
  existing_tab = main_window.find_tab_by_title("Population Analysis")
307
  if existing_tab:
308
  main_window.view.tab_widget.setCurrentWidget(existing_tab)
309
  main_window._resize_for_tab("Population Analysis")
310
  else:
311
- population_analysis_controller = self.global_settings.get_population_analysis_window()
312
  main_window.open_new_tab("Population Analysis", population_analysis_controller)
313
  except Exception as e:
314
- show_error(self.global_settings, "Error in open_population_analysis_widget() in Home", str(e))
315
-
316
- def launch_populate_fna_files(self):
317
- # Implementation for launching populate FNA files
318
- pass
319
 
320
- def open_ncbi_window(self):
321
  try:
322
- ncbi_controller = self.global_settings.get_ncbi_window()
323
- self.global_settings.main_window.open_new_tab("NCBI Download Tool", ncbi_controller)
324
  except Exception as e:
325
- show_error(self.global_settings, "Error in open_ncbi_window() in main", str(e))
326
 
327
- def _handle_db_files_changed(self, changes):
328
- """Handle database file changes"""
329
- try:
330
- # Reload model data if necessary
331
- self.model.update_for_file_changes(changes)
332
-
333
- # Update UI if needed
334
- if (FileChangeType.CSPR_ADDED in changes or
335
- FileChangeType.CSPR_REMOVED in changes):
336
- # Update both organism and endonuclease combo boxes
337
- organism_to_endonuclease = self.model.get_organism_to_endonuclease()
338
- self.view.update_combo_box_organism(sorted(organism_to_endonuclease.keys()))
339
- self.update_combo_box_endonuclease()
340
-
341
- if (FileChangeType.GBFF_ADDED in changes or
342
- FileChangeType.GBFF_REMOVED in changes):
343
- self.view.update_combo_box_annotation_files(self.model.get_annotation_files())
344
-
345
- except Exception as e:
346
- show_error(self.global_settings, "Error handling database changes", str(e))
347
-
348
- def _handle_db_validation_changed(self, is_valid, message):
349
- """Handle database validation state changes"""
350
- try:
351
- if not is_valid:
352
- self.view.show_warning("Database Warning", message)
353
-
354
- # Update UI elements based on validation state
355
- self.view.push_button_find_view_targets.setEnabled(is_valid)
356
- self.view.push_button_multitargeting_analysis.setEnabled(is_valid)
357
- self.view.push_button_population_analysis.setEnabled(is_valid)
358
-
359
- # Log the validation state change
360
- self.logger.debug(f"Database validation state changed to: {is_valid}")
361
-
362
- except Exception as e:
363
- self.logger.error(f"Error handling database validation change: {str(e)}")
364
-
365
- def _handle_db_state_changed(self, is_valid, message, changes):
366
- """Handle database state changes"""
367
  try:
368
- if not is_valid:
369
- show_error(self.global_settings, "Database Warning", message)
370
- return
371
-
372
- # Always reload model data when database state changes
373
- self.model.load_data()
374
 
375
- # Update all combo boxes
376
- organism_to_endonuclease = self.model.get_organism_to_endonuclease()
377
- self.view.update_combo_box_organism(sorted(organism_to_endonuclease.keys()))
378
- self.update_combo_box_endonuclease()
379
- self.view.update_combo_box_annotation_files(self.model.get_annotation_files())
380
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  except Exception as e:
382
- show_error(self.global_settings, "Error handling database state change", str(e))
383
 
384
- def _check_and_update_home_tab(self, index):
385
- if self.global_settings.main_window.view.tab_widget.tabText(index) == "Home":
386
- self.load_combo_box_data()
387
- # Disconnect after updating to avoid unnecessary updates
388
- self.global_settings.main_window.view.tab_widget.currentChanged.disconnect(self._check_and_update_home_tab)
389
-
390
- def get_organism_to_endonuclease(self):
391
- return self.model.get_organism_to_endonuclease()
392
-
393
- def get_annotation_files(self):
394
- return self.model.get_annotation_files()
395
-
396
- def get_annotation_file(self):
397
- return self.view.get_annotation_file()
398
-
399
- def _on_annotation_file_changed(self, new_file):
400
- """Handle changes to the annotation file selection"""
401
- self.global_settings.set_current_annotation_file(new_file)
402
-
403
- def handle_search_type_change(self):
404
- """Update UI elements based on search type"""
405
- try:
406
- search_type = self.view.get_search_type()
407
-
408
- # Update button text
409
- if search_type in ['position', 'sequence']:
410
- self.view.push_button_find_view_targets.setText("View Targets")
411
- else: # 'feature'
412
- self.view.push_button_find_view_targets.setText("Find Targets")
413
-
414
- except Exception as e:
415
- self.logger.error(f"Error updating search type UI: {str(e)}")
416
 
 
7
  import time
8
  from views.LoadingDialog import LoadingDialog
9
  from PyQt6.QtWidgets import QApplication
10
+ from PyQt6.QtCore import Qt, QSize
11
 
12
  class HomeWindowController:
13
  def __init__(self, global_settings):
14
+ self.settings = global_settings
15
+ self.logger = self.settings.get_logger()
16
+ self.is_active = True
17
+
18
  try:
19
+ self.view = HomeWindowView(self.settings)
20
+ self.model = HomeWindowModel(self.settings)
21
+ self._setup_connections()
22
+ self._init_ui()
 
 
 
 
23
  except Exception as e:
24
+ show_error(self.settings, "Error initializing HomeWindowController", str(e))
25
 
26
+ def deactivate(self):
27
+ """Cleanup controller when deactivated"""
28
  try:
29
+ self.is_active = False
30
+ if hasattr(self, 'view'):
31
+ # Safely disconnect signals
32
+ try:
33
+ self.settings.db_manager.db_state_changed.disconnect(self._on_db_state_changed)
34
+ except (TypeError, RuntimeError):
35
+ # Signal wasn't connected or already disconnected
36
+ pass
37
+
38
+ try:
39
+ self.settings.db_manager.db_files_changed.disconnect(self._on_db_files_changed)
40
+ except (TypeError, RuntimeError):
41
+ # Signal wasn't connected or already disconnected
42
+ pass
43
+
44
+ # Delete view reference
45
+ delattr(self, 'view')
46
+
47
+ except Exception as e:
48
+ self.logger.error(f"Error in deactivate: {str(e)}")
49
+
50
+ def _init_ui(self):
51
+ """Initialize UI with current data"""
52
+ try:
53
+ self._update_ui_with_model_data()
54
  self.handle_search_type_change()
55
  except Exception as e:
56
+ show_error(self.settings, "Error initializing UI in HomeWindowController", str(e))
57
 
58
+ def _update_ui_with_model_data(self):
59
+ """Update all UI elements with current model data"""
60
  try:
61
+ # Get all required data at once
62
+ organism_data = self.model.get_organism_to_endonuclease()
 
63
  annotation_files = self.model.get_annotation_files()
64
 
65
+ # Update UI elements
66
+ self._update_organism_selection(organism_data)
67
+ self._update_annotation_files(annotation_files)
68
+ except Exception as e:
69
+ show_error(self.settings, "Error updating UI with model data", str(e))
70
+
71
+ def _update_organism_selection(self, organism_data, preserve_selection=True):
72
+ """Update organism and its dependent endonuclease selection"""
73
+ try:
74
+ # Store current selection if needed
75
+ current_organism = self.view.combo_box_organism.currentText() if preserve_selection else ""
76
 
77
+ # Block signals during update
78
+ with self._block_signals(self.view.combo_box_organism):
79
+ # Update organisms
80
+ self.view.update_combo_box_organism(sorted(organism_data.keys()))
81
+
82
+ # Restore or select first item
83
+ if preserve_selection and current_organism in organism_data:
84
+ self.view.combo_box_organism.setCurrentText(current_organism)
85
 
86
+ # Update endonuclease based on current organism
87
+ self._update_endonuclease_for_organism(organism_data)
88
+ except Exception as e:
89
+ self.logger.error(f"Error updating organism selection: {str(e)}")
90
+
91
+ def _update_endonuclease_for_organism(self, organism_data):
92
+ """Update endonuclease combo box based on current organism"""
93
+ try:
94
+ selected_organism = self.view.combo_box_organism.currentText()
95
+ endonucleases = organism_data.get(selected_organism, [])
96
+ self.logger.debug(f"Updating endonuclease combo box for organism {selected_organism} with endonuclease: {endonucleases} in Main window")
97
+ self.view.update_combo_box_endonuclease(endonucleases)
98
+ except Exception as e:
99
+ self.logger.error(f"Error updating endonuclease selection: {str(e)}")
100
+
101
+ def _update_annotation_files(self, annotation_files):
102
+ """Update annotation files combo box"""
103
+ try:
104
  self.view.update_combo_box_annotation_files(annotation_files)
 
105
  except Exception as e:
106
+ self.logger.error(f"Error updating annotation files: {str(e)}")
107
 
108
+ def _on_organism_changed(self, _):
109
+ """Handle organism combo box changes"""
110
+ self._update_endonuclease_for_organism(self.model.get_organism_to_endonuclease())
 
 
111
 
112
+ def _setup_connections(self):
113
  try:
114
  # grpNavigationMenu
115
+ self.view.push_button_new_genome.clicked.connect(self.open_new_genome)
116
+ self.view.push_button_new_endonuclease.clicked.connect(self.open_new_endonuclease)
117
+ self.view.push_button_multitargeting_analysis.clicked.connect(self.open_multitargeting_analysis)
118
+ self.view.push_button_population_analysis.clicked.connect(self.open_population_analysis)
119
 
120
  # grpStep1
121
+ self.view.combo_box_organism.currentIndexChanged.connect(self._on_organism_changed)
122
 
123
  # grpStep2
124
+ self.view.push_button_ncbi_file_search.clicked.connect(self.open_ncbi)
125
 
126
  # grpStep3
127
  self.view.radio_button_feature.clicked.connect(self.handle_search_type_change)
 
131
 
132
  # Add connection for annotation file changes
133
  self.view.combo_box_local_annotation_files.currentTextChanged.connect(self._on_annotation_file_changed)
134
+
135
+ # Add connections for database changes
136
+ self.settings.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
137
+ self.settings.db_manager.db_state_changed.connect(self._on_db_state_changed)
138
+ self.settings.db_manager.db_files_changed.connect(self._on_db_files_changed)
139
+
140
  except Exception as e:
141
+ show_error(self.settings, "Error setting up connections in HomeWindowController", str(e))
 
142
 
143
+ def _on_db_files_changed(self, changes):
144
+ """Handle database file changes"""
 
145
  try:
146
+ # Reload model data if necessary
147
+ self.model.update_for_file_changes(changes)
148
 
149
+ # Update UI based on change type
150
+ if self._should_update_organisms(changes):
151
+ self._update_organism_selection(self.model.get_organism_to_endonuclease())
152
+
153
+ if self._should_update_annotations(changes):
154
+ self._update_annotation_files(self.model.get_annotation_files())
155
+
156
+ except Exception as e:
157
+ show_error(self.settings, "Error handling database changes", str(e))
158
+
159
+ @staticmethod
160
+ def _should_update_organisms(changes):
161
+ """Check if organisms need to be updated based on changes"""
162
+ return (FileChangeType.CSPR_ADDED in changes or
163
+ FileChangeType.CSPR_REMOVED in changes)
164
+
165
+ @staticmethod
166
+ def _should_update_annotations(changes):
167
+ """Check if annotations need to be updated based on changes"""
168
+ return (FileChangeType.GBFF_ADDED in changes or
169
+ FileChangeType.GBFF_REMOVED in changes)
170
+
171
+ class _block_signals:
172
+ """Context manager for blocking Qt signals"""
173
+ def __init__(self, widget):
174
+ self.widget = widget
175
+
176
+ def __enter__(self):
177
+ self.widget.blockSignals(True)
178
+ return self.widget
179
+
180
+ def __exit__(self, exc_type, exc_val, exc_tb):
181
+ self.widget.blockSignals(False)
182
+
183
+ def refresh_data(self):
184
+ """Refresh all data and update UI"""
185
+ try:
186
+ if not self.is_active or not hasattr(self, 'view'):
187
+ return
188
+
189
+ self.logger.debug("Refreshing home window data")
190
+ self.model.load_data()
191
+ self._update_ui_with_model_data()
192
+
193
+ except Exception as e:
194
+ self.logger.error(f"Error refreshing home window data: {str(e)}")
195
+
196
+ def _on_db_state_changed(self, is_valid, message, changes):
197
+ """Handle database state changes"""
198
+ try:
199
+ if not self.is_active or not hasattr(self, 'view'):
200
+ return
201
 
202
+ self.logger.debug(f"Database state changed - Valid: {is_valid}, Message: {message}")
203
+ if is_valid:
204
+ self.refresh_data()
205
+ except Exception as e:
206
+ self.logger.error(f"Error handling database state change: {str(e)}")
207
+
208
+ def _check_and_update_home_tab(self, index):
209
+ if self.settings.main_window.view.tab_widget.tabText(index) == "Home":
210
+ self.load_combo_box_data()
211
+ # Disconnect after updating to avoid unnecessary updates
212
+ self.settings.main_window.view.tab_widget.currentChanged.disconnect(self._check_and_update_home_tab)
213
+
214
+ def get_organism_to_endonuclease(self):
215
+ return self.model.get_organism_to_endonuclease()
216
+
217
+ def get_annotation_files(self):
218
+ return self.model.get_annotation_files()
219
+
220
+ def get_annotation_file(self):
221
+ return self.view.get_annotation_file()
222
+
223
+ def _on_annotation_file_changed(self, new_file):
224
+ """Handle changes to the annotation file selection"""
225
+ self.logger.debug(f"Current annotation file changed to: {new_file}")
226
+ self.settings.set_current_annotation_file(new_file)
227
+
228
+ def handle_search_type_change(self):
229
+ """Update UI elements based on search type"""
230
+ try:
231
+ search_type = self.view.get_search_type()
232
+
233
+ # Update button text
234
+ if search_type in ['position', 'sequence']:
235
+ self.view.push_button_find_view_targets.setText("View Targets")
236
+ else: # 'feature'
237
+ self.view.push_button_find_view_targets.setText("Find Targets")
238
+
239
+ except Exception as e:
240
+ self.logger.error(f"Error updating search type UI: {str(e)}")
241
+
242
+ def _on_db_validation_changed(self, is_valid, message):
243
+ """Handle database validation changes"""
244
+ try:
245
+ if self.is_active and hasattr(self, 'view'):
246
+ if is_valid:
247
+ self.refresh_data()
248
  except Exception as e:
249
+ self.logger.error(f"Error handling database validation change: {str(e)}")
250
 
251
  def open_view_targets(self, input_data):
252
  try:
 
258
 
259
  try:
260
  # Create find targets controller to use its model
261
+ find_targets_controller = self.settings.get_find_targets_window()
262
 
263
  # For position searches, handle each query separately
264
  if input_data['search_type'] == 'position':
 
298
  QApplication.processEvents()
299
 
300
  # Close existing View Targets tab if it exists
301
+ main_window = self.settings.main_window
302
  existing_tab = main_window.find_tab_by_title("View Targets")
303
  if existing_tab:
304
  tab_index = main_window.view.tab_widget.indexOf(existing_tab)
 
308
  # Create view targets controller
309
  loading_dialog.set_message("Creating view targets...", 90)
310
  QApplication.processEvents()
311
+ view_targets_controller = self.settings.get_view_targets_window()
312
 
313
  view_targets_controller.load_guides(
314
  targets,
 
334
  loading_dialog.close()
335
 
336
  except Exception as e:
337
+ self.settings.logger.error(f"Error opening view targets directly: {str(e)}")
338
+ show_error(self.settings, "Error", f"Could not open view targets: {str(e)}")
339
 
340
+ def open_find_targets(self):
341
  """Open find targets module for non-position searches"""
342
  try:
343
  # Show loading dialog
 
347
  QApplication.processEvents()
348
 
349
  try:
350
+ # Find all existing Find Targets tabs
351
+ main_window = self.settings.main_window
352
+ existing_tabs = []
353
+ tab_numbers = []
354
+
355
+ # Get all tab titles
356
+ for i in range(main_window.view.tab_widget.count()):
357
+ tab_title = main_window.view.tab_widget.tabText(i)
358
+ if tab_title.startswith("Find Targets"):
359
+ existing_tabs.append(tab_title)
360
+ # Extract number if it exists
361
+ if tab_title != "Find Targets":
362
+ try:
363
+ num = int(tab_title.split()[-1])
364
+ tab_numbers.append(num)
365
+ except ValueError:
366
+ continue
367
+
368
+ # Determine new tab number
369
+ new_tab_number = 1
370
+ if tab_numbers:
371
+ new_tab_number = max(tab_numbers) + 1
372
 
373
  loading_dialog.set_progress(40)
374
 
375
  # Create new find targets controller and load data
376
+ find_targets_controller = self.settings.get_find_targets_window()
377
  input_data = self.view.get_find_targets_input()
378
  loading_dialog.set_progress(60)
379
 
380
  find_targets_controller.find_targets(input_data)
381
  loading_dialog.set_progress(80)
382
 
383
+ # Open new Find Targets tab with number if not the first one
384
+ tab_title = "Find Targets" if not existing_tabs else f"Find Targets {new_tab_number}"
385
+ self.settings.main_window.open_new_tab(tab_title, find_targets_controller)
386
  loading_dialog.set_progress(100)
387
 
388
  finally:
389
  loading_dialog.close()
390
 
391
  except Exception as e:
392
+ show_error(self.settings, "Error in open_find_targets() in Home", str(e))
393
 
394
+ def open_new_genome(self):
 
 
 
 
395
  try:
396
+ main_window = self.settings.main_window
397
  existing_tab = main_window.find_tab_by_title("New Genome")
398
 
399
  if existing_tab:
400
  main_window.view.tab_widget.setCurrentWidget(existing_tab)
401
  main_window._resize_for_tab("New Genome")
402
  else:
403
+ new_genome_controller = self.settings.get_new_genome_window()
404
  main_window.open_new_tab("New Genome", new_genome_controller)
405
  except Exception as e:
406
+ show_error(self.settings, "Error in open_new_genome() in Home", str(e))
407
 
408
+ def open_new_endonuclease(self):
409
  try:
410
+ # Create new endonuclease controller
411
+ new_endonuclease_controller = self.settings.get_new_endonuclease_window()
412
+
413
+ # Get the window from the controller
414
+ window = new_endonuclease_controller.view
415
+
416
+ # Set window properties
417
+ window.setWindowModality(Qt.WindowModality.ApplicationModal) # Make it modal
418
+ window.setMinimumSize(QSize(500, 650)) # Smaller minimum size
419
+ window.resize(QSize(600, 650)) # Set initial size
420
+
421
+ # Get the screen where the main window is
422
+ main_window = self.settings.main_window.view
423
+ screen = main_window.screen()
424
+ if not screen:
425
+ screen = QtWidgets.QApplication.primaryScreen()
426
+
427
+ # Get the available geometry of the screen (accounts for taskbars/docks)
428
+ screen_geometry = screen.availableGeometry()
429
+
430
+ # Calculate the center point of the screen
431
+ center_point = screen_geometry.center()
432
+
433
+ # Center the window on screen
434
+ window_geometry = window.frameGeometry()
435
+ window_geometry.moveCenter(center_point)
436
+ window.move(window_geometry.topLeft())
437
+
438
+ # Show the window
439
+ window.show()
440
+ window.raise_()
441
+ window.activateWindow()
442
+
443
+ # Store reference to prevent garbage collection
444
+ self._current_new_endonuclease_window = new_endonuclease_controller
445
+
446
+ self.logger.debug("New Endonuclease window opened successfully")
447
  except Exception as e:
448
+ show_error(self.settings, "Error opening new endonuclease window", str(e))
449
 
450
+ def open_multitargeting_analysis(self):
451
  try:
452
  start_time = time.time()
453
  self.logger.debug("Starting multitargeting analysis module launch")
454
 
455
+ main_window = self.settings.main_window
456
  existing_tab = main_window.find_tab_by_title("Multitargeting Analysis")
457
 
458
  tab_check_time = time.time()
 
464
  self.logger.debug(f"Switched to existing tab: {time.time() - tab_check_time:.2f} seconds")
465
  else:
466
  controller_start = time.time()
467
+ multitargeting_controller = self.settings.get_multitargeting_window()
468
  self.logger.debug(f"Controller creation took: {time.time() - controller_start:.2f} seconds")
469
 
470
  tab_open_start = time.time()
 
473
 
474
  self.logger.debug(f"Total multitargeting module launch took: {time.time() - start_time:.2f} seconds")
475
  except Exception as e:
476
+ show_error(self.settings, "Error in open_multitargeting_analysis() in Home", str(e))
477
 
478
+ def open_population_analysis(self):
479
  try:
480
+ main_window = self.settings.main_window
481
  existing_tab = main_window.find_tab_by_title("Population Analysis")
482
  if existing_tab:
483
  main_window.view.tab_widget.setCurrentWidget(existing_tab)
484
  main_window._resize_for_tab("Population Analysis")
485
  else:
486
+ population_analysis_controller = self.settings.get_population_analysis_window()
487
  main_window.open_new_tab("Population Analysis", population_analysis_controller)
488
  except Exception as e:
489
+ show_error(self.settings, "Error in open_population_analysis() in Home", str(e))
 
 
 
 
490
 
491
+ def open_ncbi(self):
492
  try:
493
+ ncbi_controller = self.settings.get_ncbi_window()
494
+ self.settings.main_window.open_new_tab("NCBI Download Tool", ncbi_controller)
495
  except Exception as e:
496
+ show_error(self.settings, "Error in open_ncbi() in main", str(e))
497
 
498
+ # Event Handlers
499
+ def gather_settings(self):
500
+ """Process input data and direct to appropriate view"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  try:
502
+ input_data = self.view.get_find_targets_input()
 
 
 
 
 
503
 
504
+ if input_data['search_type'] == 'sequence':
505
+ sequence = input_data['search_query'].strip()
506
+ if len(sequence) < 100:
507
+ QMessageBox.warning(
508
+ self.view,
509
+ "Sequence Too Short",
510
+ "The sequence given is too small. At least 100 characters are required."
511
+ )
512
+ return
513
+ if len(sequence) > 10000:
514
+ QMessageBox.warning(
515
+ self.view,
516
+ "Sequence Too Long",
517
+ "The sequence given is too large. Maximum allowed length is 10,000 base pairs."
518
+ )
519
+ return
520
+ self.open_view_targets(input_data)
521
+ elif input_data['search_type'] == 'position':
522
+ self.open_view_targets(input_data)
523
+ else:
524
+ self.open_find_targets()
525
  except Exception as e:
526
+ show_error(self.settings, "Error in gather_settings", str(e))
527
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
 
src/controllers/MainWindowController copy.py DELETED
@@ -1,262 +0,0 @@
1
- import os
2
- from PyQt6 import QtWidgets, QtCore
3
- from PyQt6.QtWidgets import QMainWindow
4
- from views.MainWindowView import MainWindowView
5
- from models.MainWindowModel import MainWindowModel
6
- from controllers.MultitargetingWindowController import MultitargetingWindowController
7
- from utils.ui import show_error, show_message, scale_ui, center_ui, position_window
8
- from utils.web import ncbi_page, repo_page, ncbi_blast_page
9
- from PyQt6.QtCore import QObject
10
-
11
- class MainWindowController(QObject):
12
- def __init__(self, global_settings):
13
- super().__init__()
14
- self.global_settings = global_settings
15
- self.logger = global_settings.get_logger()
16
- try:
17
- self.model = MainWindowModel(global_settings)
18
- self.view = MainWindowView(global_settings)
19
- # self.setup_connections()
20
- # self.init_ui()
21
- self.show()
22
- except Exception as e:
23
- show_error(global_settings, "Error initializing MainWindowController", str(e))
24
- raise
25
-
26
- def setup_connections(self):
27
- try:
28
- # menuBar
29
- self.view.action_change_directory.triggered.connect(self.change_database_directory)
30
- # self.view.action_exit.triggered.connect(self.close)
31
- # self.view.action_open_genome_browser.triggered.connect(self.open_genome_browser)
32
- self.view.action_open_repository.triggered.connect(self.open_repository_website)
33
- self.view.action_open_NCBI_BLAST.triggered.connect(self.open_ncbi_blast_website)
34
- self.view.action_open_NCBI.triggered.connect(self.open_ncbi_website)
35
-
36
- # grpNavigationMenu
37
- self.view.push_button_new_genome.clicked.connect(self.open_new_genome_widget)
38
- self.view.push_button_new_endonuclease.clicked.connect(self.open_new_endonuclease_widget)
39
- self.view.push_button_multitargeting_analysis.clicked.connect(self.open_multitargeting_analysis_widget)
40
- self.view.push_button_population_analysis.clicked.connect(self.open_population_analysis_widget)
41
-
42
- # grpStep1
43
- self.view.combo_box_organism.currentIndexChanged.connect(self.update_combo_box_endonuclease)
44
-
45
- # grpStep2
46
- self.view.push_button_ncbi_file_search.clicked.connect(self.open_ncbi_window)
47
-
48
- # grpStep3
49
- self.view.radio_button_feature.clicked.connect(self.toggle_annotation)
50
- self.view.radio_button_position.clicked.connect(self.toggle_annotation)
51
- self.view.push_button_find_targets.clicked.connect(self.gather_settings)
52
- self.view.push_button_view_targets.clicked.connect(self.view_results)
53
- self.view.push_button_generate_library.clicked.connect(self.prep_gen_lib)
54
- # self.view.theme_toggle_button.clicked.connect(self.toggle_theme)
55
-
56
- # Custom title bar connections
57
- self.view.theme_toggle_button.clicked.connect(self.toggle_theme)
58
- self.view.minimize_button.clicked.connect(self.view.showMinimized)
59
- self.view.maximize_button.clicked.connect(self.toggle_maximize)
60
- self.view.close_button.clicked.connect(self.view.close)
61
- except Exception as e:
62
- show_error(self.global_settings, "Error setting up connections in MainWindowController", str(e))
63
-
64
- def init_ui(self):
65
- try:
66
- self.view.push_button_view_targets.setEnabled(False)
67
- self.view.push_button_generate_library.setEnabled(False)
68
- self.load_combo_box_data()
69
- self.view.reset_progress_bar()
70
- except Exception as e:
71
- show_error(self.global_settings, "Error initializing UI in MainWindowController", str(e))
72
-
73
- def load_combo_box_data(self):
74
- try:
75
- self.model.load_data()
76
- self.update_combo_box_data()
77
- except Exception as e:
78
- show_error(self.global_settings, "Error loading dropdown data in MainWindowController", str(e))
79
-
80
- def update_combo_box_data(self):
81
- organism_to_endonuclease = self.model.get_organism_to_endonuclease()
82
- annotation_files = self.model.get_annotation_files()
83
-
84
- self.logger.debug(f"Updating Organisms combo box with organisms: {organism_to_endonuclease.keys()} in Main window")
85
- self.view.update_combo_box_organism(list(organism_to_endonuclease.keys()))
86
-
87
- self.update_combo_box_endonuclease()
88
-
89
- self.logger.debug(f"Updating Annotation files combo box with annotation files: {annotation_files} in Main window")
90
- self.view.update_combo_box_annotation_files(annotation_files)
91
-
92
- def update_combo_box_endonuclease(self):
93
- selected_organism = self.view.combo_box_organism.currentText()
94
- endonuclease = self.model.get_organism_to_endonuclease().get(selected_organism, [])
95
- self.logger.debug(f"Updating endonuclease combo box for organism {selected_organism} with endonuclease: {endonuclease} in Main window")
96
- self.view.update_combo_box_endonuclease(endonuclease)
97
-
98
- # Event Handlers
99
- def gather_settings(self):
100
- # Implementation for gathering settings
101
- pass
102
-
103
- def view_results(self):
104
- # Implementation for viewing results
105
- pass
106
-
107
- def toggle_annotation(self):
108
- # Implementation for toggling annotation
109
- pass
110
-
111
- def prep_gen_lib(self):
112
- # Implementation for preparing gene library
113
- pass
114
-
115
- def open_new_genome_widget(self):
116
- try:
117
- new_genome_window = self.global_settings.new_genome_window
118
- self._open_widget("New Genome", new_genome_window.view)
119
- except Exception as e:
120
- show_error(self.global_settings, "Error in open_new_genome_widget() in main", e)
121
-
122
- def open_new_endonuclease_widget(self):
123
- try:
124
- new_endo_window = self.global_settings.new_endonuclease_window
125
- self._open_widget("New Endonuclease", new_endo_window.view)
126
- except Exception as e:
127
- show_error(self.global_settings, "Error in open_new_endonuclease_widget() in main", str(e))
128
-
129
- def open_multitargeting_analysis_widget(self):
130
- try:
131
- multitargeting_window = self.global_settings.multitargeting_window
132
- self._open_widget("Multitargeting Analysis", multitargeting_window)
133
- except Exception as e:
134
- show_error(self.global_settings, "Error in open_multitargeting_analysis_widget() in main", str(e))
135
-
136
- def open_population_analysis_widget(self):
137
- try:
138
- population_analysis_window = self.global_settings.population_analysis_window
139
- self._open_widget("Population Analysis", population_analysis_window)
140
- except Exception as e:
141
- show_error(self.global_settings, "Error in open_population_analysis_widget() in main", str(e))
142
-
143
- def launch_populate_fna_files(self):
144
- # Implementation for launching populate FNA files
145
- pass
146
-
147
- def open_ncbi_window(self):
148
- try:
149
- ncbi_window = self.global_settings.ncbi_window
150
- self._open_widget("NCBI Window", ncbi_window.view)
151
- except Exception as e:
152
- show_error(self.global_settings, "Error in open_ncbi_window() in main", str(e))
153
-
154
- def toggle_theme(self):
155
- try:
156
- self.global_settings.toggle_dark_mode()
157
- self.view.update_theme_icon()
158
- self.apply_theme()
159
- except Exception as e:
160
- self.logger.error(f"Error toggling theme: {str(e)}", exc_info=True)
161
- show_error(self.global_settings, "Error toggling theme", str(e))
162
-
163
- def toggle_maximize(self):
164
- if self.view.isMaximized():
165
- self.view.showNormal()
166
- else:
167
- self.view.showMaximized()
168
-
169
- def apply_theme(self):
170
- # Apply theme to all widgets
171
- if self.global_settings.is_dark_mode():
172
- # Apply dark theme
173
- self.view.setStyleSheet("""
174
- QWidget { background-color: #2b2b2b; color: #ffffff; }
175
- QPushButton { background-color: #3a3a3a; border: 1px solid #5a5a5a; }
176
- QPushButton:hover { background-color: #4a4a4a; }
177
- QLineEdit, QTextEdit, QPlainTextEdit { background-color: #3a3a3a; border: 1px solid #5a5a5a; }
178
- QComboBox { background-color: #3a3a3a; border: 1px solid #5a5a5a; }
179
- QMenuBar { background-color: #2b2b2b; }
180
- QMenuBar::item:selected { background-color: #3a3a3a; }
181
- QMenu { background-color: #2b2b2b; }
182
- QMenu::item:selected { background-color: #3a3a3a; }
183
- """)
184
- else:
185
- # Apply light theme
186
- self.view.setStyleSheet("""
187
- QWidget { background-color: #f0f0f0; color: #000000; }
188
- QPushButton { background-color: #e0e0e0; border: 1px solid #c0c0c0; }
189
- QPushButton:hover { background-color: #d0d0d0; }
190
- QLineEdit, QTextEdit, QPlainTextEdit { background-color: #ffffff; border: 1px solid #c0c0c0; }
191
- QComboBox { background-color: #ffffff; border: 1px solid #c0c0c0; }
192
- QMenuBar { background-color: #f0f0f0; }
193
- QMenuBar::item:selected { background-color: #e0e0e0; }
194
- QMenu { background-color: #f0f0f0; }
195
- QMenu::item:selected { background-color: #e0e0e0; }
196
- """)
197
-
198
- # Utility Functions
199
- def some_long_running_task(self):
200
- if self.view.progress_bar:
201
- self.view.reset_progress()
202
- # ... (perform task steps)
203
- if self.view.progress_bar:
204
- self.view.set_progress(50)
205
- # ... (more task steps)
206
- if self.view.progress_bar:
207
- self.view.set_progress(100)
208
-
209
- def show(self):
210
- try:
211
- saved_position = self.global_settings.load_window_position("main_window")
212
- if saved_position:
213
- self.view.move(saved_position)
214
- else:
215
- center_ui(self.view)
216
- self.view.show()
217
- self.apply_theme()
218
- except Exception as e:
219
- self.global_settings.logger.error(f"Error showing main window: {str(e)}", exc_info=True)
220
- show_error(self.global_settings, "Error showing main window", e)
221
-
222
- def closeEvent(self, event):
223
- self.global_settings.save_window_position("main_window", self.view.pos())
224
- event.accept()
225
-
226
- def change_database_directory(self):
227
- try:
228
- new_directory = QtWidgets.QFileDialog.getExistingDirectory(
229
- self.view, "Select Database Directory", self.global_settings.CSPR_DB,
230
- QtWidgets.QFileDialog.Option.ShowDirsOnly
231
- )
232
- if new_directory:
233
- if self.global_settings.validate_db_path(new_directory):
234
- self.global_settings.save_database_path(new_directory)
235
- self.global_settings.initialize_app_directories()
236
- self.load_combo_box_data()
237
- show_message(12, QtWidgets.QMessageBox.Icon.Information,
238
- "Success", "Database directory changed successfully.")
239
- else:
240
- show_message(12, QtWidgets.QMessageBox.Icon.Warning,
241
- "Invalid Directory", "The selected directory does not contain valid CSPR files.")
242
- except Exception as e:
243
- self.logger.error(f"Error changing database directory: {str(e)}", exc_info=True)
244
- show_error(self.global_settings, "Error changing database directory", str(e))
245
-
246
- def open_ncbi_website(self):
247
- ncbi_page()
248
-
249
- def open_repository_website(self):
250
- repo_page()
251
-
252
- def open_ncbi_blast_website(self):
253
- ncbi_blast_page()
254
-
255
- def _open_widget(self, title, widget) -> None:
256
- index = self.view.stacked_widget.indexOf(widget)
257
- if index == -1:
258
- self.view.stacked_widget.addWidget(widget)
259
- index = self.view.stacked_widget.indexOf(widget)
260
- self.view.stacked_widget.setCurrentIndex(index)
261
- # Make sure the stacked widget is visible
262
- self.view.stacked_widget.show()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/controllers/MainWindowController.py CHANGED
@@ -22,12 +22,28 @@ class MainWindowController(LoggingMixin):
22
  self.startup_size = QSize(750, 550)
23
  self.current_tab = None
24
  self.previous_size = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  try:
27
  self.view = MainWindowView(self.settings)
28
  self._setup_connections()
29
  self._init_ui()
30
  self.settings.check_and_emit_first_time_startup()
 
 
31
  except Exception as e:
32
  self.log_error("__init__", e)
33
  show_error(self.settings, "Error initializing MainWindowController", str(e))
@@ -43,21 +59,25 @@ class MainWindowController(LoggingMixin):
43
 
44
  # Tab bar
45
  self.view.tab_widget.tab_closed.connect(self._on_tab_closed)
46
- self.view.tab_widget.tabCloseRequested.connect(self._close_tab)
47
  self.view.tab_widget.currentChanged.connect(self._on_current_tab_changed)
48
 
49
  self.settings.first_time_startup.connect(self._handle_first_time_startup)
50
 
51
  # Add Button Menu
52
- # self.view.action_new_genome.triggered.connect(self.open_new_genome_tab)
53
- # self.view.action_new_endonuclease.triggered.connect(self.open_new_endonuclease_tab)
54
 
55
  # Settings Menu
56
  self.view.action_toggle_theme.triggered.connect(self._toggle_theme)
57
 
 
 
 
 
58
  def _init_ui(self):
59
  self.log_method_call("_init_ui")
60
 
 
61
  if self.is_first_time_startup:
62
  self.log_info("First time startup detected. Opening startup tab.")
63
  self._open_startup_tab()
@@ -66,21 +86,46 @@ class MainWindowController(LoggingMixin):
66
  db_path = self.settings.get_db_path()
67
  is_valid, message = self.settings.validate_db_path(db_path)
68
 
69
- if db_path and is_valid:
70
- self.log_info(f"Database path is valid: {db_path}")
71
- self._open_home_tab()
72
- else:
73
  self.log_warning(f"Invalid database path: {db_path}. {message}")
74
- self._open_startup_tab()
 
 
 
 
 
75
 
76
  def _handle_first_time_startup(self):
77
  self.log_info("First time startup signal received")
78
  self.is_first_time_startup = True
79
  self._open_startup_tab()
80
 
81
- def _open_startup_tab(self):
82
  try:
83
- self.startup_controller = self.settings.get_startup_window()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  self.open_new_tab("Startup", self.startup_controller)
85
  except Exception as e:
86
  self.log_error("_open_startup_tab", e)
@@ -109,12 +154,29 @@ class MainWindowController(LoggingMixin):
109
  self._center_window()
110
 
111
  def _center_window(self):
 
112
  try:
113
- center_point = QtGui.QGuiApplication.primaryScreen().availableGeometry().center()
 
 
 
 
 
 
 
 
 
 
 
114
  frame_geometry = self.view.frameGeometry()
 
 
115
  frame_geometry.moveCenter(center_point)
 
 
116
  self.view.move(frame_geometry.topLeft())
117
- self.log_debug(f"Window centered at {self.view.pos()}")
 
118
  except Exception as e:
119
  self.log_error("_center_window", e)
120
  show_error(self.settings, "Error centering window", str(e))
@@ -141,22 +203,87 @@ class MainWindowController(LoggingMixin):
141
  show_error(self.settings, "Error changing database directory", str(e))
142
 
143
  def _handle_invalid_directory(self, new_directory, message):
 
 
 
 
 
 
 
 
 
 
 
144
  reply = QtWidgets.QMessageBox.question(
145
  self.view,
146
  "Invalid Directory",
147
- f"The selected directory does not contain valid CSPR files: {message}\n\n"
148
- "Would you like to analyze a new genome in this directory?",
149
  QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
150
  QtWidgets.QMessageBox.StandardButton.No
151
  )
152
 
 
 
153
  if reply == QtWidgets.QMessageBox.StandardButton.Yes:
154
- self.settings.save_db_path(new_directory)
155
- self.settings.update_db_state()
156
- self.open_new_genome_tab()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  else:
 
158
  show_message("Operation Cancelled", "Database directory change cancelled.")
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  def _process_valid_directory(self, new_directory):
161
  try:
162
  self.settings.save_db_path(new_directory)
@@ -215,7 +342,7 @@ class MainWindowController(LoggingMixin):
215
  if existing_tab:
216
  self.log_debug(f"Tab '{title}' already exists, switching to it")
217
  self.view.tab_widget.setCurrentWidget(existing_tab)
218
- self._resize_for_tab(title)
219
  return
220
 
221
  # Create widget from content
@@ -237,87 +364,85 @@ class MainWindowController(LoggingMixin):
237
  self.view.tab_widget.setCurrentIndex(index)
238
  self.tab_widgets['widgets'][title] = wrapper
239
 
240
- self._resize_for_tab(title)
241
  self.log_info(f"Tab '{title}' opened successfully at index {index}")
242
 
243
  except Exception as e:
244
  self.log_error("open_new_tab", e)
245
  show_error(self.settings, f"Error opening tab '{title}'", str(e))
246
 
247
- def _resize_for_tab(self, title):
 
248
  try:
 
 
 
249
  if title == "Startup":
250
- # For Startup tab, set fixed size but keep window controls
251
- self.view.setFixedSize(self.startup_size)
252
- elif title in ["View Targets", "Multitargeting Analysis"]:
253
- # Store current size before applying constraints
254
- if self.current_tab not in ["View Targets", "Multitargeting Analysis"]:
 
255
  self.previous_size = self.view.size()
256
 
257
- # Set minimum dimensions for these tabs
258
- min_width = 1300
259
- min_height = 800
260
-
261
  # Calculate new dimensions
262
- new_width = max(self.view.width(), min_width)
263
- new_height = max(self.view.height(), min_height)
264
 
265
  # Only resize if dimensions need to increase
266
  if new_width > self.view.width() or new_height > self.view.height():
267
  self.view.resize(QSize(new_width, new_height))
268
 
269
- # Set minimum size constraints
270
- self.view.setMinimumSize(QSize(min_width, min_height))
271
- self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
272
- else:
273
- # For all other tabs
274
- self.view.setMinimumSize(QSize(400, 300))
275
  self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
276
 
277
- # Restore previous size if available and coming from View Targets or Multi-targeting Analysis
278
- if self.current_tab in ["View Targets", "Multitargeting Analysis"] and self.previous_size:
279
- self.view.resize(self.previous_size)
 
 
 
 
 
280
  elif self.current_tab == "Startup" or self.view.size() == self.startup_size:
281
  self.view.resize(self.shared_tab_size)
282
 
283
  # Update the current tab
284
  self.current_tab = title
285
 
 
 
 
 
286
  except Exception as e:
287
  self.log_error("_resize_for_tab", e)
288
 
289
  def _close_tab(self, index):
290
  """Handle tab closure using CloseableTabWidget"""
291
- if 0 <= index < self.view.tab_widget.count():
292
- title = self.view.tab_widget.tabText(index)
293
-
294
- # Store size before closing View Targets tab
295
- if title == "View Targets":
296
- self.previous_size = self.view.size()
297
-
298
- # Let CloseableTabWidget handle the widget cleanup
299
- self.view.tab_widget.closeTab(index)
300
-
301
- # Clean up references
302
- if title in self.tab_widgets['widgets']:
303
- del self.tab_widgets['widgets'][title]
304
- if title in self.tab_widgets['controllers']:
305
- del self.tab_widgets['controllers'][title]
306
-
307
- self.logger.debug(f"Closed tab '{title}' at index {index}")
308
-
309
- # Handle post-close operations
310
- if title == "New Genome":
311
- home_tab = self.find_tab_by_title("Home")
312
- if home_tab:
313
- home_controller = self.settings.get_home_window()
314
- home_controller.refresh_data()
315
 
316
- # Resize for the current tab
317
- if self.view.tab_widget.count() > 0:
318
- new_index = self.view.tab_widget.currentIndex()
319
- new_tab_title = self.view.tab_widget.tabText(new_index)
320
- self._resize_for_tab(new_tab_title)
321
 
322
  def _toggle_theme(self):
323
  try:
@@ -332,11 +457,11 @@ class MainWindowController(LoggingMixin):
332
  saved_position = self.settings.load_window_position("main_window")
333
  if saved_position:
334
  self.view.move(saved_position)
335
- else:
336
- # center_ui(self.view)
337
- pass
338
  self.view.show()
339
  self.view.apply_theme()
 
 
 
340
  except Exception as e:
341
  self.log_error("show", e)
342
  show_error(self.settings, "Error showing main window", e)
@@ -410,20 +535,101 @@ class MainWindowController(LoggingMixin):
410
  if old_tab_title and old_tab_title != "Startup":
411
  self.previous_size = self.view.size()
412
 
413
- self._resize_for_tab(new_tab_title)
414
 
415
  except Exception as e:
416
  self.log_error("_on_current_tab_changed", e)
417
 
418
- def open_new_endonuclease_tab(self):
419
- """Opens the new endonuclease window"""
420
  try:
 
421
  new_endonuclease_controller = self.settings.get_new_endonuclease_window()
422
- new_endonuclease_controller.view.show() # Show as window instead of tab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  except Exception as e:
424
- self.log_error("open_new_endonuclease_tab", e)
425
  show_error(self.settings, "Error opening new endonuclease window", str(e))
426
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
 
428
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
 
 
22
  self.startup_size = QSize(750, 550)
23
  self.current_tab = None
24
  self.previous_size = None
25
+ # Add flag to track dialog state
26
+ self._cspr_deletion_dialog_shown = False
27
+
28
+ # Define minimum sizes for different tab types
29
+ self.tab_min_sizes = {
30
+ "Startup": QSize(750, 550), # Startup has a fixed size
31
+ "View Targets": QSize(1300, 800), # Large tabs
32
+ "Multitargeting Analysis": QSize(1300, 800),
33
+ "Population Analysis": QSize(1000, 700), # Medium-large tabs
34
+ "Find Targets": QSize(1000, 700),
35
+ "New Genome": QSize(800, 600), # Medium tabs
36
+ "Home": QSize(800, 600),
37
+ "default": QSize(600, 500) # Default minimum size for any other tab
38
+ }
39
 
40
  try:
41
  self.view = MainWindowView(self.settings)
42
  self._setup_connections()
43
  self._init_ui()
44
  self.settings.check_and_emit_first_time_startup()
45
+ # Center the window after initialization
46
+ self._center_window()
47
  except Exception as e:
48
  self.log_error("__init__", e)
49
  show_error(self.settings, "Error initializing MainWindowController", str(e))
 
59
 
60
  # Tab bar
61
  self.view.tab_widget.tab_closed.connect(self._on_tab_closed)
62
+ self.view.tab_widget.tab_closing.connect(self._close_tab)
63
  self.view.tab_widget.currentChanged.connect(self._on_current_tab_changed)
64
 
65
  self.settings.first_time_startup.connect(self._handle_first_time_startup)
66
 
67
  # Add Button Menu
68
+ self.view.action_new_endonuclease.triggered.connect(self.open_new_endonuclease_window)
 
69
 
70
  # Settings Menu
71
  self.view.action_toggle_theme.triggered.connect(self._toggle_theme)
72
 
73
+ # Database state changes
74
+ self.settings.db_manager.db_state_changed.connect(self._on_db_state_changed)
75
+ self.settings.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
76
+
77
  def _init_ui(self):
78
  self.log_method_call("_init_ui")
79
 
80
+ # Check if it's first time startup
81
  if self.is_first_time_startup:
82
  self.log_info("First time startup detected. Opening startup tab.")
83
  self._open_startup_tab()
 
86
  db_path = self.settings.get_db_path()
87
  is_valid, message = self.settings.validate_db_path(db_path)
88
 
89
+ if not is_valid:
 
 
 
90
  self.log_warning(f"Invalid database path: {db_path}. {message}")
91
+ # Always open startup tab for invalid paths, keeping the existing path
92
+ self._open_startup_tab(keep_db_path=True)
93
+ return # Add explicit return to prevent further execution
94
+
95
+ self.log_info(f"Database path is valid: {db_path}")
96
+ self._open_home_tab()
97
 
98
  def _handle_first_time_startup(self):
99
  self.log_info("First time startup signal received")
100
  self.is_first_time_startup = True
101
  self._open_startup_tab()
102
 
103
+ def _open_startup_tab(self, keep_db_path=False):
104
  try:
105
+ # First safely deactivate any existing controllers
106
+ for title, controller in list(self.tab_widgets['controllers'].items()):
107
+ try:
108
+ if hasattr(controller, 'deactivate'):
109
+ controller.deactivate()
110
+ except Exception as e:
111
+ self.logger.error(f"Error deactivating controller for {title}: {str(e)}")
112
+
113
+ # Force close all tabs
114
+ while self.view.tab_widget.count() > 0:
115
+ try:
116
+ widget = self.view.tab_widget.widget(0)
117
+ self.view.tab_widget.removeTab(0)
118
+ if widget:
119
+ widget.deleteLater()
120
+ except Exception as e:
121
+ self.logger.error(f"Error closing tab: {str(e)}")
122
+
123
+ # Clear tab tracking dictionaries
124
+ self.tab_widgets['widgets'].clear()
125
+ self.tab_widgets['controllers'].clear()
126
+
127
+ # Then open the startup tab
128
+ self.startup_controller = self.settings.get_startup_window(keep_db_path=keep_db_path)
129
  self.open_new_tab("Startup", self.startup_controller)
130
  except Exception as e:
131
  self.log_error("_open_startup_tab", e)
 
154
  self._center_window()
155
 
156
  def _center_window(self):
157
+ """Center the window on the current screen"""
158
  try:
159
+ # Get the current screen where the window is or the primary screen
160
+ window_screen = self.view.screen()
161
+ if not window_screen:
162
+ window_screen = QtGui.QGuiApplication.primaryScreen()
163
+
164
+ # Get the geometry of the screen
165
+ screen_geometry = window_screen.availableGeometry()
166
+
167
+ # Calculate the center point
168
+ center_point = screen_geometry.center()
169
+
170
+ # Get the window geometry
171
  frame_geometry = self.view.frameGeometry()
172
+
173
+ # Move the window's center to the screen's center
174
  frame_geometry.moveCenter(center_point)
175
+
176
+ # Move the window to the calculated position
177
  self.view.move(frame_geometry.topLeft())
178
+
179
+ self.log_debug(f"Window centered on screen at {self.view.pos()}")
180
  except Exception as e:
181
  self.log_error("_center_window", e)
182
  show_error(self.settings, "Error centering window", str(e))
 
203
  show_error(self.settings, "Error changing database directory", str(e))
204
 
205
  def _handle_invalid_directory(self, new_directory, message):
206
+ """Handle invalid directory selection with option to analyze new genomes"""
207
+ self.logger.debug("Entering _handle_invalid_directory") # Add entry log
208
+
209
+ # Always show error for non-existent directories
210
+ if message == "The selected directory does not exist.":
211
+ self.logger.debug("Directory does not exist, showing error") # Add debug log
212
+ show_error(self.settings, "Invalid Directory", message)
213
+ return
214
+
215
+ self.logger.debug("Showing analyze new genome dialog") # Add debug log
216
+ # Show dialog for analyzing new genome
217
  reply = QtWidgets.QMessageBox.question(
218
  self.view,
219
  "Invalid Directory",
220
+ "Would you like to analyze a new genome in this directory? testing",
 
221
  QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
222
  QtWidgets.QMessageBox.StandardButton.No
223
  )
224
 
225
+ self.logger.debug(f"User reply to analyze new genome: {reply == QtWidgets.QMessageBox.StandardButton.Yes}") # Add debug log
226
+
227
  if reply == QtWidgets.QMessageBox.StandardButton.Yes:
228
+ self.logger.debug("User chose to analyze new genome") # Add debug log
229
+ # Temporarily disconnect validation signal to prevent warning
230
+ self.settings.db_manager.db_validation_changed.disconnect()
231
+ try:
232
+ # Set directory change flag
233
+ self.settings.db_manager.is_changing_directory = True
234
+ self.logger.debug(f"Starting directory change process to: {new_directory}")
235
+
236
+ # Save the new path without switching to startup tab
237
+ self.settings.save_db_path(new_directory)
238
+ self.settings.update_db_state()
239
+
240
+ # Store the new directory for later use
241
+ self._pending_directory_change = new_directory
242
+
243
+ # Open new genome tab without closing other tabs
244
+ self.logger.debug("Opening new genome tab") # Add debug log
245
+ self.open_new_genome_tab()
246
+
247
+ # Connect to the new genome tab's completion signal
248
+ new_genome_tab = self.find_tab_by_title("New Genome")
249
+ if new_genome_tab and new_genome_tab in self.tab_widgets['controllers']:
250
+ new_genome_controller = self.tab_widgets['controllers']["New Genome"]
251
+ new_genome_controller.process_completed.connect(self._on_new_genome_completed)
252
+ self.logger.debug("Connected to new genome completion signal") # Add debug log
253
+
254
+ except Exception as e:
255
+ self.logger.error(f"Error in _handle_invalid_directory: {str(e)}") # Add error log
256
+ raise
257
+ finally:
258
+ # Reconnect the signal after operation is complete
259
+ self.settings.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
260
+ self.logger.debug("Reconnected validation signal") # Add debug log
261
  else:
262
+ self.logger.debug("User cancelled new genome analysis") # Add debug log
263
  show_message("Operation Cancelled", "Database directory change cancelled.")
264
 
265
+ def _on_new_genome_completed(self):
266
+ """Handle completion of new genome analysis"""
267
+ try:
268
+ if hasattr(self, '_pending_directory_change'):
269
+ # Show success message
270
+ show_message(
271
+ "Database Directory Change Complete",
272
+ f"Successfully changed database directory to:\n{self._pending_directory_change}",
273
+ QtWidgets.QMessageBox.Icon.Information
274
+ )
275
+
276
+ # Refresh home tab
277
+ home_tab = self.find_tab_by_title("Home")
278
+ if home_tab and home_tab in self.tab_widgets['controllers']:
279
+ home_controller = self.tab_widgets['controllers']["Home"]
280
+ home_controller.refresh_data()
281
+
282
+ # Clean up
283
+ delattr(self, '_pending_directory_change')
284
+ except Exception as e:
285
+ self.logger.error(f"Error handling new genome completion: {str(e)}")
286
+
287
  def _process_valid_directory(self, new_directory):
288
  try:
289
  self.settings.save_db_path(new_directory)
 
342
  if existing_tab:
343
  self.log_debug(f"Tab '{title}' already exists, switching to it")
344
  self.view.tab_widget.setCurrentWidget(existing_tab)
345
+ self._resize_for_tab(title, center_window=False) # Don't center when switching to existing tab
346
  return
347
 
348
  # Create widget from content
 
364
  self.view.tab_widget.setCurrentIndex(index)
365
  self.tab_widgets['widgets'][title] = wrapper
366
 
367
+ self._resize_for_tab(title, center_window=True) # Center when opening new tab
368
  self.log_info(f"Tab '{title}' opened successfully at index {index}")
369
 
370
  except Exception as e:
371
  self.log_error("open_new_tab", e)
372
  show_error(self.settings, f"Error opening tab '{title}'", str(e))
373
 
374
+ def _resize_for_tab(self, title, center_window=True):
375
+ """Handle window resizing for different tab types"""
376
  try:
377
+ # Get the minimum size for this tab type
378
+ min_size = self.tab_min_sizes.get(title, self.tab_min_sizes["default"])
379
+
380
  if title == "Startup":
381
+ # Startup tab has a fixed size
382
+ self.view.setFixedSize(min_size)
383
+ else:
384
+ # Store current size before applying constraints if coming from a different size category
385
+ current_min_size = self.tab_min_sizes.get(self.current_tab, self.tab_min_sizes["default"])
386
+ if current_min_size != min_size:
387
  self.previous_size = self.view.size()
388
 
 
 
 
 
389
  # Calculate new dimensions
390
+ new_width = max(self.view.width(), min_size.width())
391
+ new_height = max(self.view.height(), min_size.height())
392
 
393
  # Only resize if dimensions need to increase
394
  if new_width > self.view.width() or new_height > self.view.height():
395
  self.view.resize(QSize(new_width, new_height))
396
 
397
+ # Set size constraints
398
+ self.view.setMinimumSize(min_size)
 
 
 
 
399
  self.view.setMaximumSize(QtCore.QSize(16777215, 16777215))
400
 
401
+ # Restore previous size if available and coming from a larger minimum size tab
402
+ if self.current_tab and self.previous_size:
403
+ current_min_size = self.tab_min_sizes.get(self.current_tab, self.tab_min_sizes["default"])
404
+ if current_min_size.width() > min_size.width() or current_min_size.height() > min_size.height():
405
+ # Only restore if the previous size is larger than our minimum
406
+ if (self.previous_size.width() >= min_size.width() and
407
+ self.previous_size.height() >= min_size.height()):
408
+ self.view.resize(self.previous_size)
409
  elif self.current_tab == "Startup" or self.view.size() == self.startup_size:
410
  self.view.resize(self.shared_tab_size)
411
 
412
  # Update the current tab
413
  self.current_tab = title
414
 
415
+ # Center the window after resizing only if requested
416
+ if center_window:
417
+ self._center_window()
418
+
419
  except Exception as e:
420
  self.log_error("_resize_for_tab", e)
421
 
422
  def _close_tab(self, index):
423
  """Handle tab closure using CloseableTabWidget"""
424
+ try:
425
+ if 0 <= index < self.view.tab_widget.count():
426
+ title = self.view.tab_widget.tabText(index)
427
+
428
+ # Safely deactivate controller if it exists
429
+ if title in self.tab_widgets['controllers']:
430
+ try:
431
+ controller = self.tab_widgets['controllers'][title]
432
+ if hasattr(controller, 'deactivate'):
433
+ controller.deactivate()
434
+ except Exception as e:
435
+ self.logger.error(f"Error deactivating controller for {title}: {str(e)}")
436
+
437
+ # Clean up references
438
+ if title in self.tab_widgets['widgets']:
439
+ del self.tab_widgets['widgets'][title]
440
+ if title in self.tab_widgets['controllers']:
441
+ del self.tab_widgets['controllers'][title]
 
 
 
 
 
 
442
 
443
+ self.logger.debug(f"Closed tab '{title}' at index {index}")
444
+ except Exception as e:
445
+ self.logger.error(f"Error in _close_tab: {str(e)}")
 
 
446
 
447
  def _toggle_theme(self):
448
  try:
 
457
  saved_position = self.settings.load_window_position("main_window")
458
  if saved_position:
459
  self.view.move(saved_position)
 
 
 
460
  self.view.show()
461
  self.view.apply_theme()
462
+ # Center the window after showing it
463
+ self._center_window()
464
+ print("Window initialized")
465
  except Exception as e:
466
  self.log_error("show", e)
467
  show_error(self.settings, "Error showing main window", e)
 
535
  if old_tab_title and old_tab_title != "Startup":
536
  self.previous_size = self.view.size()
537
 
538
+ self._resize_for_tab(new_tab_title, center_window=False)
539
 
540
  except Exception as e:
541
  self.log_error("_on_current_tab_changed", e)
542
 
543
+ def open_new_endonuclease_window(self):
544
+ """Opens the new endonuclease as a separate window"""
545
  try:
546
+ # Create the controller
547
  new_endonuclease_controller = self.settings.get_new_endonuclease_window()
548
+
549
+ # Get the window from the controller
550
+ window = new_endonuclease_controller.view
551
+
552
+ # Set window properties
553
+ window.setWindowModality(Qt.WindowModality.ApplicationModal) # Make it modal
554
+ window.setMinimumSize(QSize(800, 600)) # Set minimum size
555
+
556
+ # Center the window relative to the main window
557
+ main_window_center = self.view.geometry().center()
558
+ window_geometry = window.frameGeometry()
559
+ window_geometry.moveCenter(main_window_center)
560
+ window.move(window_geometry.topLeft())
561
+
562
+ # Show the window
563
+ window.show()
564
+ window.raise_()
565
+ window.activateWindow()
566
+
567
+ # Store reference to prevent garbage collection
568
+ self._current_new_endonuclease_window = new_endonuclease_controller
569
+
570
+ self.log_info("New Endonuclease window opened successfully")
571
  except Exception as e:
572
+ self.log_error("open_new_endonuclease_window", e)
573
  show_error(self.settings, "Error opening new endonuclease window", str(e))
574
 
575
+ def _on_db_validation_changed(self, is_valid, message):
576
+ """Handle database validation state changes"""
577
+ if not is_valid:
578
+ self.logger.debug(f"Database validation failed: {message}")
579
+
580
+ # Only show dialog if we're not in startup tab
581
+ startup_tab = self.find_tab_by_title("Startup")
582
+ if startup_tab and self.view.tab_widget.currentWidget() == startup_tab:
583
+ return
584
+
585
+ # Switch to startup tab for invalid paths (including when files are deleted)
586
+ # but keep the current path
587
+ if not startup_tab:
588
+ self._open_startup_tab(keep_db_path=True)
589
+ return
590
+
591
+ else:
592
+ self._cspr_deletion_dialog_shown = False # Reset flag
593
+ if "Successfully changed database directory to:" in message:
594
+ show_message(
595
+ "Database Directory Change Complete",
596
+ message,
597
+ QtWidgets.QMessageBox.Icon.Information
598
+ )
599
+ # Refresh home tab if it exists
600
+ home_tab = self.find_tab_by_title("Home")
601
+ if home_tab and home_tab in self.tab_widgets['controllers']:
602
+ home_controller = self.tab_widgets['controllers']["Home"]
603
+ home_controller.refresh_data()
604
 
605
+ def _on_db_state_changed(self, is_valid, message, changes):
606
+ """Handle database state changes"""
607
+ try:
608
+ self.logger.debug(f"Database state changed - Valid: {is_valid}, Message: {message}, Changes: {changes}")
609
+
610
+ # If path becomes invalid (including when files are deleted), switch to startup tab
611
+ if not is_valid:
612
+ # Skip only if we're already in startup tab
613
+ startup_tab = self.find_tab_by_title("Startup")
614
+ if startup_tab and self.view.tab_widget.currentWidget() == startup_tab:
615
+ return
616
+
617
+ # Skip only if we're in the process of changing directory for new genome
618
+ if self.settings.db_manager.is_changing_directory:
619
+ return
620
+
621
+ # Otherwise, switch to startup tab with current path
622
+ self._open_startup_tab(keep_db_path=True)
623
+ return
624
+
625
+ # Handle other state changes
626
+ if changes:
627
+ # Refresh home tab if it exists
628
+ home_tab = self.find_tab_by_title("Home")
629
+ if home_tab and home_tab in self.tab_widgets['controllers']:
630
+ home_controller = self.tab_widgets['controllers']["Home"]
631
+ home_controller.refresh_data()
632
+
633
+ except Exception as e:
634
+ self.logger.error(f"Error handling database state change: {str(e)}")
635
 
src/controllers/NCBIWindowController.py CHANGED
@@ -14,15 +14,28 @@ class NCBIWindowController:
14
  def __init__(self, settings):
15
  self.settings = settings
16
  try:
 
17
  self.logger = self.settings.get_logger()
 
 
 
 
18
  self.model = NCBIWindowModel(settings)
 
 
 
 
19
  self.view = NCBIWindowView(settings)
 
20
 
21
  # Connect to the initialization complete signal
22
  self.view.initialization_complete.connect(self.setup_connections)
23
 
24
  self._init_ui()
 
 
25
  except Exception as e:
 
26
  show_error(self.settings, "Error initializing NCBIWindowController", str(e))
27
 
28
  def setup_connections(self):
@@ -134,7 +147,7 @@ class NCBIWindowController:
134
  self.logger.info(f"Processing ID: {id}")
135
 
136
  urls = self.model.get_download_url(id, self.view.radio_button_collections_genbank.isChecked())
137
- self.logger.info(f"Download URLs for ID {id}: {urls}")
138
 
139
  if not urls:
140
  self.logger.warning(f"No download URL found for ID: {id}")
 
14
  def __init__(self, settings):
15
  self.settings = settings
16
  try:
17
+ start_time = time.time()
18
  self.logger = self.settings.get_logger()
19
+ self.logger.debug("Starting NCBIWindowController initialization")
20
+
21
+ # Log model initialization time
22
+ model_start = time.time()
23
  self.model = NCBIWindowModel(settings)
24
+ self.logger.debug(f"Model initialization took: {time.time() - model_start:.2f} seconds")
25
+
26
+ # Log view initialization time
27
+ view_start = time.time()
28
  self.view = NCBIWindowView(settings)
29
+ self.logger.debug(f"View initialization took: {time.time() - view_start:.2f} seconds")
30
 
31
  # Connect to the initialization complete signal
32
  self.view.initialization_complete.connect(self.setup_connections)
33
 
34
  self._init_ui()
35
+
36
+ self.logger.debug(f"Total NCBIWindowController initialization took: {time.time() - start_time:.2f} seconds")
37
  except Exception as e:
38
+ self.logger.error(f"Error initializing NCBIWindowController: {str(e)}")
39
  show_error(self.settings, "Error initializing NCBIWindowController", str(e))
40
 
41
  def setup_connections(self):
 
147
  self.logger.info(f"Processing ID: {id}")
148
 
149
  urls = self.model.get_download_url(id, self.view.radio_button_collections_genbank.isChecked())
150
+ self.logger.info(f"Download URLs for ID {id}: {urls} in database")
151
 
152
  if not urls:
153
  self.logger.warning(f"No download URL found for ID: {id}")
src/controllers/NewGenomeWindowController.py CHANGED
@@ -3,11 +3,13 @@ from models.NewGenomeWindowModel import NewGenomeWindowModel
3
  from views.NewGenomeWindowView import NewGenomeWindowView
4
  from utils.ui import show_message, show_error
5
  import os
 
6
 
7
  class NewGenomeWindowController:
8
  def __init__(self, global_settings):
9
  self.settings = global_settings
10
  self.logger = global_settings.get_logger()
 
11
 
12
  try:
13
  self.model = NewGenomeWindowModel(self.settings)
@@ -66,25 +68,37 @@ class NewGenomeWindowController:
66
  # self._load_endonuclease_settings()
67
 
68
  def _handle_reset(self):
69
- self.view.line_edit_organism_name.clear()
70
- self.view.line_edit_strain.clear()
71
- self.view.line_edit_organism_code.clear()
 
72
 
73
- self.model.file = ""
74
- self.view.line_edit_selected_file.clear()
75
- self.view.line_edit_selected_file.setPlaceholderText("Selected FASTA/FNA File")
76
 
77
- self.view.reset_table_widget_jobs()
78
- self.view.reset_progress_bar_jobs()
79
 
80
- # Reinitialize the process
81
- if self.job_process.state() != QtCore.QProcess.ProcessState.NotRunning:
82
- self.job_process.kill()
83
- self._initialize_process()
84
 
85
- # Reset the model
86
- self.model.reset_progress()
87
- self.model.jobs.clear()
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  def _add_job_to_table(self):
90
  organism_name = self.view.get_organism_name()
@@ -123,16 +137,28 @@ class NewGenomeWindowController:
123
 
124
  def _browse_fasta_file(self):
125
  file_dialog = QtWidgets.QFileDialog()
126
- database_dir = self.settings.get_db_path()
127
- file_path, _ = file_dialog.getOpenFileName(self.view, "Choose a File", database_dir, "FASTA Files (*.fa *.fna *.fasta)")
 
 
 
 
 
 
 
 
128
  if file_path:
129
  if self.model.validate_fasta_file(file_path):
130
  self.model.file = file_path
131
  self.view.set_selected_file(file_path)
 
132
  else:
133
- show_message(fontSize=12, icon=QtWidgets.QMessageBox.Icon.Critical,
134
- title="File Selection Error",
135
- message="You have selected an incorrect type of file. Please choose a FASTA/FNA file.")
 
 
 
136
 
137
  def _remove_selected_job(self):
138
  job_identifier = self.view.get_selected_job_identifier()
@@ -235,48 +261,73 @@ class NewGenomeWindowController:
235
  self.view.table_widget_jobs.viewport().update()
236
 
237
  def _handle_job_completion(self, exit_code=None, exit_status=None):
238
- self.logger.debug(f"Process finished with exit code: {exit_code}")
239
-
240
- # Log any remaining output
241
- remaining_output = self.job_process.readAllStandardOutput().data().decode()
242
- if remaining_output:
243
- self.logger.debug(f"Final process output: {remaining_output}")
244
-
245
- remaining_error = self.job_process.readAllStandardError().data().decode()
246
- if remaining_error:
247
- self.logger.error(f"Final process error output: {remaining_error}")
248
-
249
- if hasattr(self, 'job_indexes') and self.job_indexes:
250
- completed_row_index = self.job_indexes.pop(0)
251
-
252
- # Check if output files were created
253
- expected_cspr_file = os.path.join(self.settings.get_db_path(), f"{self.model.get_job_name(completed_row_index)}.cspr")
254
- if os.path.exists(expected_cspr_file):
255
- self.logger.debug(f"CSPR file created successfully: {expected_cspr_file}")
256
- else:
257
- self.logger.error(f"Expected CSPR file not found: {expected_cspr_file}")
258
 
259
- # Set job as completed
260
- self.view.set_job_completed(completed_row_index)
 
 
261
 
262
- # Update model's completed jobs count and progress bar
263
- total_progress = self.model.increment_completed_jobs()
264
- if total_progress is not None:
265
- self.view.set_progress_bar_jobs(total_progress)
266
- else:
267
- self.logger.warning("Received None for total_progress")
268
 
269
- if self.job_indexes:
270
- next_row_index = self.job_indexes[0]
271
- self._run_job(next_row_index)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  else:
273
- self.logger.info("All queued jobs completed")
274
- self.view.set_progress_bar_jobs(100)
275
- self.settings.update_db_state()
276
- else:
277
- self.logger.warning("No job indexes found or all jobs completed")
278
 
279
- self.view.table_widget_jobs.viewport().update()
 
 
 
280
 
281
  def _reset_table_widget_jobs(self):
282
  self.view.reset_table_widget_jobs()
@@ -293,10 +344,13 @@ class NewGenomeWindowController:
293
 
294
  # Connect to the initialization complete signal
295
  def on_init_complete():
 
296
  if organism_name:
297
  ncbi_controller.view.line_edit_organism.setText(organism_name)
298
  if strain_name:
299
  ncbi_controller.view.line_edit_strain.setText(strain_name)
 
 
300
 
301
  # Connect the signal
302
  ncbi_controller.view.initialization_complete.connect(on_init_complete)
@@ -308,9 +362,6 @@ class NewGenomeWindowController:
308
  show_error(self.settings, "Error opening NCBI module", str(e))
309
  self.logger.error(f"Failed to open NCBI module: {str(e)}")
310
 
311
- # def _on_cspr_files_created(self):
312
- # self.settings.update_db_state()
313
-
314
  def _load_initial_endonuclease_settings(self):
315
  initial_endonuclease = self.view.get_selected_endonuclease()
316
  print(f"Initial endonuclease: {initial_endonuclease}")
 
3
  from views.NewGenomeWindowView import NewGenomeWindowView
4
  from utils.ui import show_message, show_error
5
  import os
6
+ from PyQt6.QtCore import pyqtSignal
7
 
8
  class NewGenomeWindowController:
9
  def __init__(self, global_settings):
10
  self.settings = global_settings
11
  self.logger = global_settings.get_logger()
12
+ self.directory_change_completed = pyqtSignal(str)
13
 
14
  try:
15
  self.model = NewGenomeWindowModel(self.settings)
 
68
  # self._load_endonuclease_settings()
69
 
70
  def _handle_reset(self):
71
+ try:
72
+ self.view.line_edit_organism_name.clear()
73
+ self.view.line_edit_strain.clear()
74
+ self.view.line_edit_organism_code.clear()
75
 
76
+ self.model.file = ""
77
+ self.view.line_edit_selected_file.clear()
78
+ self.view.line_edit_selected_file.setPlaceholderText("Selected FASTA/FNA File")
79
 
80
+ self.view.reset_table_widget_jobs()
81
+ self.view.reset_progress_bar_jobs()
82
 
83
+ # Reinitialize the process
84
+ if self.job_process.state() != QtCore.QProcess.ProcessState.NotRunning:
85
+ self.job_process.kill()
86
+ self._initialize_process()
87
 
88
+ # Reset the model
89
+ self.model.reset_progress()
90
+ self.model.jobs.clear()
91
+
92
+ # Cancel any pending database path change
93
+ self.settings.db_manager.pending_db_path = None
94
+ self.logger.debug("Cancelled pending database path change")
95
+
96
+ # Show confirmation to user
97
+ show_message("Reset Complete",
98
+ "Form has been reset and any pending database changes have been cancelled.")
99
+ except Exception as e:
100
+ self.logger.error(f"Error in handle reset: {str(e)}")
101
+ show_error(self.settings, "Error", str(e))
102
 
103
  def _add_job_to_table(self):
104
  organism_name = self.view.get_organism_name()
 
137
 
138
  def _browse_fasta_file(self):
139
  file_dialog = QtWidgets.QFileDialog()
140
+ database_dir = self.settings.db_manager.get_active_db_path()
141
+ self.logger.debug(f"Opening file dialog with directory: {database_dir}")
142
+
143
+ file_path, _ = file_dialog.getOpenFileName(
144
+ self.view,
145
+ "Choose a File",
146
+ database_dir,
147
+ "FASTA Files (*.fa *.fna *.fasta)"
148
+ )
149
+
150
  if file_path:
151
  if self.model.validate_fasta_file(file_path):
152
  self.model.file = file_path
153
  self.view.set_selected_file(file_path)
154
+ self.logger.debug(f"Selected valid FASTA file: {file_path}")
155
  else:
156
+ show_message(
157
+ fontSize=12,
158
+ icon=QtWidgets.QMessageBox.Icon.Critical,
159
+ title="File Selection Error",
160
+ message="You have selected an incorrect type of file. Please choose a FASTA/FNA file."
161
+ )
162
 
163
  def _remove_selected_job(self):
164
  job_identifier = self.view.get_selected_job_identifier()
 
261
  self.view.table_widget_jobs.viewport().update()
262
 
263
  def _handle_job_completion(self, exit_code=None, exit_status=None):
264
+ """Handle process completion and job status updates"""
265
+ try:
266
+ self.logger.debug(f"Process finished with exit code: {exit_code}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
+ # Log any remaining output
269
+ remaining_output = self.job_process.readAllStandardOutput().data().decode()
270
+ if remaining_output:
271
+ self.logger.debug(f"Final process output: {remaining_output}")
272
 
273
+ remaining_error = self.job_process.readAllStandardError().data().decode()
274
+ if remaining_error:
275
+ self.logger.error(f"Final process error output: {remaining_error}")
 
 
 
276
 
277
+ if hasattr(self, 'job_indexes') and self.job_indexes:
278
+ completed_row_index = self.job_indexes.pop(0)
279
+
280
+ # Get the job name for the completed job
281
+ job_name = self.model.get_job_name(completed_row_index)
282
+ expected_cspr_file = os.path.join(self.settings.get_db_path(), f"{job_name}.cspr")
283
+
284
+ if exit_code == 0 and os.path.exists(expected_cspr_file):
285
+ self.logger.debug(f"CSPR file created successfully: {expected_cspr_file}")
286
+ self.view.set_job_completed(completed_row_index)
287
+
288
+ # Update model's completed jobs count and progress bar
289
+ total_progress = self.model.increment_completed_jobs()
290
+ if total_progress is not None:
291
+ self.view.set_progress_bar_jobs(total_progress)
292
+ else:
293
+ self.logger.warning("Received None for total_progress")
294
+
295
+ # If we're in directory change mode, trigger a database state update
296
+ if self.settings.db_manager.is_changing_directory:
297
+ self.logger.debug("Directory change in progress - triggering state update")
298
+ self.settings.db_manager.update_db_state()
299
+ else:
300
+ error_msg = f"Job failed: CSPR file not found or process error (exit code: {exit_code})"
301
+ self.logger.error(error_msg)
302
+ show_error(self.settings, f"Job Failed: {job_name}", error_msg)
303
+
304
+ # Process next job if any
305
+ if self.job_indexes:
306
+ next_row_index = self.job_indexes[0]
307
+ self._run_job(next_row_index)
308
+ else:
309
+ self.logger.info("All queued jobs completed")
310
+ self.view.set_progress_bar_jobs(100)
311
+
312
+ # If we were changing directory, finalize the change
313
+ if self.settings.db_manager.is_changing_directory:
314
+ self.logger.debug("Finalizing directory change after successful genome analysis")
315
+ success, message = self.settings.db_manager.finalize_directory_change()
316
+ if success:
317
+ self.directory_change_completed.emit(message)
318
+ else:
319
+ self.logger.error(f"Failed to finalize directory change: {message}")
320
+ else:
321
+ # Just update the state if not changing directory
322
+ self.settings.update_db_state()
323
+
324
  else:
325
+ self.logger.warning("No job indexes found or all jobs completed")
 
 
 
 
326
 
327
+ self.view.table_widget_jobs.viewport().update()
328
+
329
+ except Exception as e:
330
+ self.logger.error(f"Error in _handle_job_completion: {str(e)}")
331
 
332
  def _reset_table_widget_jobs(self):
333
  self.view.reset_table_widget_jobs()
 
344
 
345
  # Connect to the initialization complete signal
346
  def on_init_complete():
347
+ self.logger.debug(f"NCBI window initialized, setting organism: {organism_name}, strain: {strain_name}")
348
  if organism_name:
349
  ncbi_controller.view.line_edit_organism.setText(organism_name)
350
  if strain_name:
351
  ncbi_controller.view.line_edit_strain.setText(strain_name)
352
+ # Disconnect after use to prevent multiple connections
353
+ ncbi_controller.view.initialization_complete.disconnect(on_init_complete)
354
 
355
  # Connect the signal
356
  ncbi_controller.view.initialization_complete.connect(on_init_complete)
 
362
  show_error(self.settings, "Error opening NCBI module", str(e))
363
  self.logger.error(f"Failed to open NCBI module: {str(e)}")
364
 
 
 
 
365
  def _load_initial_endonuclease_settings(self):
366
  initial_endonuclease = self.view.get_selected_endonuclease()
367
  print(f"Initial endonuclease: {initial_endonuclease}")
src/controllers/OffTargetController.py CHANGED
@@ -218,6 +218,18 @@ class OffTargetController(QObject):
218
  if endo_index >= 0:
219
  self.view.combo_box_endonuclease.setCurrentIndex(endo_index)
220
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  # Store targets for analysis
222
  if 'guides' in parameters:
223
  self._targets = parameters['guides']
 
218
  if endo_index >= 0:
219
  self.view.combo_box_endonuclease.setCurrentIndex(endo_index)
220
 
221
+ # Validate and set annotation file
222
+ if 'annotation_file' not in parameters:
223
+ raise ValueError("No annotation file provided in parameters")
224
+
225
+ annotation_file = parameters['annotation_file']
226
+ if not annotation_file:
227
+ raise ValueError("Empty annotation file path provided")
228
+
229
+ # Set annotation file in global settings
230
+ self.global_settings.set_current_annotation_file(annotation_file)
231
+ self.logger.debug(f"Set annotation file to: {annotation_file}")
232
+
233
  # Store targets for analysis
234
  if 'guides' in parameters:
235
  self._targets = parameters['guides']
src/controllers/PopulationAnalysisWindowController.py CHANGED
@@ -39,7 +39,6 @@ class PopulationAnalysisWindowController:
39
  def launch(self):
40
  try:
41
  self.logger = self.global_settings.get_logger()
42
- self.logger.info("Launching Population Analysis Window")
43
  self.get_data()
44
  except Exception as e:
45
  self.logger.error(f"Error in launch(): {str(e)}")
@@ -47,7 +46,6 @@ class PopulationAnalysisWindowController:
47
 
48
  def get_data(self):
49
  try:
50
- self.logger.info("Getting data for Population Analysis")
51
  self.fillEndo()
52
  except Exception as e:
53
  self.logger.error(f"Error in get_data(): {str(e)}")
@@ -55,7 +53,6 @@ class PopulationAnalysisWindowController:
55
 
56
  def fillEndo(self):
57
  try:
58
- self.logger.info("Starting fillEndo()")
59
  endos = self.model.load_endonucleases()
60
  self.logger.debug(f"Loaded endonucleases: {endos}")
61
 
@@ -64,7 +61,6 @@ class PopulationAnalysisWindowController:
64
  show_error(self.global_settings, "Error", "No endonucleases found")
65
  return
66
 
67
- self.logger.info(f"Updating dropdown with {len(endos)} endonucleases")
68
  self.view.update_endo_dropdown(endos.keys())
69
  self.change_endo()
70
  except Exception as e:
@@ -190,8 +186,6 @@ class PopulationAnalysisWindowController:
190
  data['pams'][majority_index], # PAM
191
  strand # Strand
192
  )
193
-
194
- self.logger.debug(f"Processed seed data: {row_data}")
195
  return row_data
196
 
197
  except Exception as e:
 
39
  def launch(self):
40
  try:
41
  self.logger = self.global_settings.get_logger()
 
42
  self.get_data()
43
  except Exception as e:
44
  self.logger.error(f"Error in launch(): {str(e)}")
 
46
 
47
  def get_data(self):
48
  try:
 
49
  self.fillEndo()
50
  except Exception as e:
51
  self.logger.error(f"Error in get_data(): {str(e)}")
 
53
 
54
  def fillEndo(self):
55
  try:
 
56
  endos = self.model.load_endonucleases()
57
  self.logger.debug(f"Loaded endonucleases: {endos}")
58
 
 
61
  show_error(self.global_settings, "Error", "No endonucleases found")
62
  return
63
 
 
64
  self.view.update_endo_dropdown(endos.keys())
65
  self.change_endo()
66
  except Exception as e:
 
186
  data['pams'][majority_index], # PAM
187
  strand # Strand
188
  )
 
 
189
  return row_data
190
 
191
  except Exception as e:
src/controllers/StartupWindowController.py CHANGED
@@ -6,10 +6,11 @@ from views.StartupWindowView import StartupWindowView
6
  import sys
7
 
8
  class StartupWindowController:
9
- def __init__(self, global_settings):
10
  self.settings = global_settings
11
  self.logger = self.settings.get_logger()
12
- self.is_active = True # Initialize is_active here
 
13
 
14
  try:
15
  self.view = StartupWindowView(self.settings)
@@ -31,10 +32,19 @@ class StartupWindowController:
31
  self.view.db_path_text_changed.connect(self._on_db_path_text_changed)
32
  self.model.db_state_updated.connect(self._on_db_state_updated)
33
  self.settings.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
 
34
  self.view.open_new_genome_requested.connect(self.open_new_genome_tab)
35
 
36
  def _on_db_path_text_changed(self, new_path):
37
- self.model.save_db_path(new_path)
 
 
 
 
 
 
 
 
38
 
39
  def _on_db_state_updated(self, is_valid, message, cspr_files):
40
  if self.is_active and hasattr(self, 'view'):
@@ -45,15 +55,63 @@ class StartupWindowController:
45
  if self.is_active and hasattr(self, 'view'):
46
  self.view.set_db_status(is_valid, message)
47
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  def _init_ui(self):
49
- db_path = self.model.get_db_path()
50
- self.logger.debug(f"Initial database path: {db_path}")
51
- self._init_db_state(db_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  def _init_db_state(self, db_path):
54
- self.view.set_db_path(db_path)
55
- is_valid, message = self.settings.validate_db_path(db_path)
56
- self.view.set_db_status(is_valid, message)
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  def _set_database_directory(self):
59
  try:
 
6
  import sys
7
 
8
  class StartupWindowController:
9
+ def __init__(self, global_settings, keep_db_path=False):
10
  self.settings = global_settings
11
  self.logger = self.settings.get_logger()
12
+ self.is_active = True
13
+ self.keep_db_path = keep_db_path
14
 
15
  try:
16
  self.view = StartupWindowView(self.settings)
 
32
  self.view.db_path_text_changed.connect(self._on_db_path_text_changed)
33
  self.model.db_state_updated.connect(self._on_db_state_updated)
34
  self.settings.db_manager.db_validation_changed.connect(self._on_db_validation_changed)
35
+ self.settings.db_manager.db_files_changed.connect(self._on_db_files_changed)
36
  self.view.open_new_genome_requested.connect(self.open_new_genome_tab)
37
 
38
  def _on_db_path_text_changed(self, new_path):
39
+ """Handle database path text changes"""
40
+ try:
41
+ # Avoid triggering save if the path hasn't actually changed
42
+ if new_path == self.model.get_db_path():
43
+ return
44
+
45
+ self.model.save_db_path(new_path)
46
+ except Exception as e:
47
+ self.logger.error(f"Error handling db path change: {str(e)}")
48
 
49
  def _on_db_state_updated(self, is_valid, message, cspr_files):
50
  if self.is_active and hasattr(self, 'view'):
 
55
  if self.is_active and hasattr(self, 'view'):
56
  self.view.set_db_status(is_valid, message)
57
 
58
+ def _on_db_files_changed(self, changes):
59
+ """Handle database file changes"""
60
+ if self.is_active and hasattr(self, 'view'):
61
+ # Re-validate the current path
62
+ db_path = self.model.get_db_path()
63
+ is_valid, message = self.settings.validate_db_path(db_path)
64
+ self.view.set_db_status(is_valid, message)
65
+
66
+ # If path is now valid and we have CSPR files, update button text
67
+ if is_valid:
68
+ self.view.push_button_go_to_home_or_new_genome.setText("Go to Home")
69
+
70
  def _init_ui(self):
71
+ """Initialize the UI with the correct database path"""
72
+ try:
73
+ # Always get the current path from settings
74
+ db_path = self.model.get_db_path()
75
+
76
+ # For true first time startup (no previous path), show default path
77
+ if self.settings.is_first_time_startup and not db_path:
78
+ db_path = self.settings.db_manager.get_default_database_path()
79
+ # For invalid path case, keep the existing path
80
+ elif not self.keep_db_path and not self.settings.is_first_time_startup:
81
+ db_path = ''
82
+
83
+ self.logger.debug(f"Initial database path: {db_path}, keep_db_path: {self.keep_db_path}, "
84
+ f"first_time_startup: {self.settings.is_first_time_startup}")
85
+
86
+ # Set the path in the view first
87
+ self.view.set_db_path(db_path)
88
+
89
+ # Then initialize the state
90
+ if db_path:
91
+ is_valid, message = self.settings.validate_db_path(db_path)
92
+ self.view.set_db_status(is_valid, message)
93
+ else:
94
+ self.view.set_db_status(False, "No directory selected")
95
+
96
+ except Exception as e:
97
+ self.logger.error(f"Error in _init_ui: {str(e)}")
98
+ raise
99
 
100
  def _init_db_state(self, db_path):
101
+ """Initialize database state"""
102
+ try:
103
+ # Only update the view's path if it's different from current
104
+ current_view_path = self.view.get_db_path()
105
+ if current_view_path != db_path:
106
+ self.view.set_db_path(db_path)
107
+
108
+ # Validate and update status
109
+ is_valid, message = self.settings.validate_db_path(db_path)
110
+ self.view.set_db_status(is_valid, message)
111
+
112
+ except Exception as e:
113
+ self.logger.error(f"Error in _init_db_state: {str(e)}")
114
+ raise
115
 
116
  def _set_database_directory(self):
117
  try:
src/controllers/ViewTargetsController.py CHANGED
@@ -8,6 +8,7 @@ import traceback
8
  from views.LoadingDialog import LoadingDialog
9
  from PyQt6.QtWidgets import QApplication
10
  from PyQt6.QtGui import QColor
 
11
 
12
  class ViewTargetsController:
13
  def __init__(self, global_settings):
@@ -31,7 +32,6 @@ class ViewTargetsController:
31
  self.view.push_button_change_location.clicked.connect(self.change_indices)
32
  self.view.push_button_reset_location.clicked.connect(self.reset_location)
33
  self.view.check_box_select_all.stateChanged.connect(self.select_all)
34
- # self.view.combo_box_gene.currentIndexChanged.connect(self.display_gene_data)
35
  self.view.gene_selected.connect(self.on_gene_selected)
36
 
37
  self.view.check_box_filter_5_prime_g_sequences.stateChanged.connect(self.refresh_guides_display)
@@ -42,12 +42,34 @@ class ViewTargetsController:
42
  """Handle view exons only checkbox state change"""
43
  try:
44
  is_checked = self.view.check_box_view_exons_only.isChecked()
45
- self.logger.debug(f"View exons only changed to: {is_checked}")
46
  self.model.set_view_exons_only(is_checked)
47
  self.refresh_gene_viewer()
48
  except Exception as e:
49
  self.logger.error(f"Error handling view exons change: {str(e)}")
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  def load_guides(self, selected_targets, organism, endonuclease, loading_dialog=None):
52
  try:
53
  self.organism = organism
@@ -62,6 +84,9 @@ class ViewTargetsController:
62
  QApplication.processEvents()
63
 
64
  try:
 
 
 
65
  loading_dialog.set_message("Loading guides...", 60)
66
  QApplication.processEvents()
67
 
@@ -92,43 +117,73 @@ class ViewTargetsController:
92
  loading_dialog.set_message("Updating display...", 80)
93
  QApplication.processEvents()
94
 
95
- self.view.display_guides_in_table(guides)
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- # Only trigger gene selection if this is the initial load
98
- if not hasattr(self, '_initial_load_complete'):
99
- loading_dialog.set_message("Loading initial gene data...", 90)
100
- QApplication.processEvents()
101
-
102
- # Get unique position names or gene IDs
103
- unique_entries = set()
104
- for target in selected_targets:
105
- if 'feature_id' in target:
106
- # For position-based searches, use the feature_id directly
107
- if "chromosome" in str(target['feature_id']):
108
- unique_entries.add(target['feature_id'])
109
- else:
110
- # For gene-based searches, get gene data and format with name
111
- locus_tag = target['feature_id']
112
- gene_data = self.model.get_gene_data(locus_tag)
113
- if gene_data and 'info' in gene_data:
114
- gene_name = gene_data['info'].get('gene_name', '')
115
- display_text = f"{locus_tag}: {gene_name}" if gene_name else locus_tag
116
- unique_entries.add(display_text)
117
 
118
- # Convert set to list for combo box
119
- entries = list(unique_entries)
120
- self.logger.debug(f"Found {len(entries)} unique entries")
121
 
 
122
  self.view.set_combo_box_gene(entries)
 
123
 
124
- # Set first entry without triggering the selection signal
125
- if entries:
126
- self.view.combo_box_gene.blockSignals(True)
127
- self.view.combo_box_gene.setCurrentIndex(0)
128
- self.view.combo_box_gene.blockSignals(False)
129
- self._load_initial_gene_data(entries[0])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- self._initial_load_complete = True
 
132
 
133
  loading_dialog.set_progress(100)
134
  QApplication.processEvents()
@@ -144,53 +199,6 @@ class ViewTargetsController:
144
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
145
  show_error(self.settings, "Error loading guides", str(e))
146
 
147
- def _load_initial_gene_data(self, selected_text):
148
- """Load initial gene data without showing loading dialog"""
149
- try:
150
- # Similar to on_gene_selected but without loading dialog
151
- if "chromosome" in selected_text and "start:" in selected_text:
152
- # Parse position from the text
153
- parts = selected_text.split(',')
154
- chrom = parts[0].split('chromosome')[1].strip() # Remove any extra colons
155
- start = int(parts[1].split('start:')[1].strip())
156
- end = int(parts[2].split('end:')[1].strip())
157
-
158
- self.view.line_edit_start_location.setText(str(start))
159
- self.view.line_edit_stop_location.setText(str(end))
160
-
161
- # Get sequence directly for position-based search
162
- sequence = self.model._get_sequence_for_position(chrom, start, end)
163
- if sequence:
164
- # Update gene viewer with sequence
165
- self.view.update_gene_viewer(sequence, [])
166
- self.logger.debug(f"Updated gene viewer with sequence of length {len(sequence)}")
167
- else:
168
- self.logger.error(f"Could not get sequence for position {start}-{end} in chromosome {chrom}")
169
-
170
- position_guides = [g for g in self.model.guides
171
- if g.get('feature_id') == selected_text]
172
- self.view.display_guides_in_table(position_guides)
173
- else:
174
- # Regular gene-based search
175
- locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
176
- sequence_data = self.model.get_gene_sequence(locus_tag)
177
- if sequence_data:
178
- self.view.line_edit_start_location.setText(str(sequence_data['start']))
179
- self.view.line_edit_stop_location.setText(str(sequence_data['end']))
180
-
181
- features = self.model.get_features_for_gene(locus_tag)
182
-
183
- self.view.update_gene_viewer(sequence_data['sequence'], features)
184
-
185
- gene_guides = [g for g in self.model.guides
186
- if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
187
- self.view.display_guides_in_table(gene_guides)
188
-
189
- self.logger.debug("Initial gene data loaded successfully")
190
-
191
- except Exception as e:
192
- self.logger.error(f"Error loading initial gene data: {str(e)}")
193
-
194
  def _on_endonuclease_changed(self, new_endonuclease):
195
  try:
196
  if new_endonuclease != self.endonuclease:
@@ -210,8 +218,6 @@ class ViewTargetsController:
210
  new_target['endonuclease'] = new_endonuclease
211
  updated_targets.append(new_target)
212
 
213
- self.logger.debug(f"Created {len(updated_targets)} updated targets for {new_endonuclease}")
214
-
215
  # Update model with new targets
216
  self.model.load_guides(updated_targets, self.organism, new_endonuclease)
217
  guides = self.model.get_guides()
@@ -226,36 +232,6 @@ class ViewTargetsController:
226
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
227
  show_error(self.settings, "Error", f"Could not change endonuclease: {str(e)}")
228
 
229
- # def load_gene_viewer(self):
230
- # try:
231
- # # Get selected gene from combo box
232
- # selected_text = self.view.combo_box_gene.currentText()
233
- # if not selected_text:
234
- # self.logger.debug("No gene selected")
235
- # return
236
-
237
- # # Extract locus tag from "locus_tag: gene_name" format
238
- # locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
239
- # self.logger.debug(f"Loading sequence for locus tag: {locus_tag}")
240
-
241
- # # Get gene sequence with padding
242
- # sequence_data = self.model.get_gene_sequence(locus_tag)
243
-
244
- # if sequence_data:
245
- # # Update gene viewer with sequence
246
- # self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
247
-
248
- # # Update location fields
249
- # self.view.line_edit_start_location.setText(str(sequence_data['start']))
250
- # self.view.line_edit_stop_location.setText(str(sequence_data['end']))
251
-
252
- # else:
253
- # self.logger.warning(f"No sequence data found for locus tag {locus_tag}")
254
-
255
- # except Exception as e:
256
- # self.logger.error(f"Error in load_gene_viewer: {str(e)}")
257
- # self.logger.error(f"Stack trace: {traceback.format_exc()}")
258
-
259
  def perform_off_target_analysis(self):
260
  """Launch off-target analysis for selected guides"""
261
  try:
@@ -279,11 +255,35 @@ class ViewTargetsController:
279
  self._handle_off_target_results
280
  )
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  # Set initial parameters based on current organism/endonuclease
283
  parameters = {
284
  'organism': self.organism,
285
  'endonuclease': self.endonuclease,
286
- 'guides': selected_guides # Pass the selected guides
 
287
  }
288
 
289
  # Initialize analysis with parameters
@@ -473,20 +473,9 @@ class ViewTargetsController:
473
  show_error(self.settings, "Error", f"Could not show scoring options: {str(e)}")
474
 
475
  def change_indices(self):
476
- """Change the start and end positions for gene viewer"""
477
  try:
478
- # Make sure gene viewer has content
479
- if not self.view.text_edit_gene_viewer.toPlainText():
480
- QMessageBox.warning(
481
- self.view,
482
- "Gene Viewer Error",
483
- "Gene Viewer display is empty! Please ensure there is sequence data to view."
484
- )
485
- return
486
-
487
- # Get current gene/position info
488
- current_gene = self.view.combo_box_gene.currentText()
489
-
490
  try:
491
  new_start = int(self.view.line_edit_start_location.text())
492
  new_end = int(self.view.line_edit_stop_location.text())
@@ -523,7 +512,10 @@ class ViewTargetsController:
523
  )
524
  return
525
 
526
- # Get sequence for new range
 
 
 
527
  if "chromosome" in current_gene and "start:" in current_gene:
528
  # For position-based searches
529
  try:
@@ -531,12 +523,14 @@ class ViewTargetsController:
531
  # Get full chromosome identifier instead of just the number
532
  chrom = parts[0].split('chromosome')[1].strip() # This will now keep the full identifier
533
 
534
- # Get sequence for new range
535
- sequence = self.model._get_sequence_for_position(chrom, new_start, new_end)
536
 
537
  if sequence:
538
- self.view.set_text_edit_gene_viewer(sequence)
539
- # Update the line edits with new positions
 
 
540
  self.view.line_edit_start_location.setText(str(new_start))
541
  self.view.line_edit_stop_location.setText(str(new_end))
542
  else:
@@ -545,44 +539,60 @@ class ViewTargetsController:
545
  except Exception as e:
546
  QMessageBox.warning(
547
  self.view,
548
- "Position Error",
549
- f"Error changing position: {str(e)}"
550
  )
551
- return
552
  else:
553
- # For feature-based searches
554
  locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
555
- gene_data = self.model.get_gene_data(locus_tag)
556
 
557
- if not gene_data or 'info' not in gene_data:
558
- QMessageBox.warning(
559
- self.view,
560
- "Gene Data Error",
561
- "Could not get gene data for the current selection."
562
- )
563
- return
564
 
565
- print(f"locus_tag: {locus_tag}, new_start: {new_start}, new_end: {new_end}")
566
-
567
- # Get new sequence for the range
568
- sequence_data = self.model.get_gene_sequence_for_range(locus_tag, new_start, new_end)
569
- if sequence_data:
570
- self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
571
- # Update the line edits with new positions
572
- self.view.line_edit_start_location.setText(str(new_start))
573
- self.view.line_edit_stop_location.setText(str(new_end))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
574
  else:
575
  QMessageBox.warning(
576
  self.view,
577
- "Sequence Error",
578
- "Could not get sequence for the specified range."
579
  )
580
- return
581
-
582
  except Exception as e:
583
- self.logger.error(f"Error in change_indices: {str(e)}")
584
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
585
- show_error(self.settings, "Error changing indices", str(e))
586
 
587
  def reset_location(self):
588
  """Reset gene viewer to the original sequence and location"""
@@ -602,12 +612,17 @@ class ViewTargetsController:
602
  # Get sequence directly using model's method
603
  sequence = self.model._get_sequence_for_position(chrom, start, end)
604
  if sequence:
605
- # Update gene viewer with sequence
606
- self.view.set_text_edit_gene_viewer(sequence)
607
 
608
- # Update location fields - subtract 1 from start to match 0-based indexing
609
  self.view.line_edit_start_location.setText(str(start + 1))
610
  self.view.line_edit_stop_location.setText(str(end))
 
 
 
 
 
611
  else:
612
  raise ValueError("Could not get sequence for position")
613
 
@@ -620,30 +635,41 @@ class ViewTargetsController:
620
  )
621
  return
622
  else:
623
- # For feature-based searches
624
  locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
625
- sequence_data = self.model.get_gene_sequence(locus_tag)
626
 
 
 
627
  if sequence_data:
628
- # Update gene viewer with sequence
629
- self.view.set_text_edit_gene_viewer(sequence_data['sequence'])
630
 
631
- # Update location fields - subtract 1 from start to match 0-based indexing
 
 
 
 
 
 
 
632
  self.view.line_edit_start_location.setText(str(sequence_data['start'] + 1))
633
  self.view.line_edit_stop_location.setText(str(sequence_data['end']))
 
 
 
 
 
634
  else:
635
- self.logger.warning(f"No sequence data found for locus tag {locus_tag}")
636
  QMessageBox.warning(
637
  self.view,
638
- "Gene Data Error",
639
- "Could not get gene sequence for resetting location."
640
  )
641
- return
642
-
643
  except Exception as e:
644
  self.logger.error(f"Error in reset_location: {str(e)}")
645
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
646
- show_error(self.settings, "Error resetting location", str(e))
647
 
648
  def select_all(self, state):
649
  try:
@@ -673,6 +699,10 @@ class ViewTargetsController:
673
  # Get current guides from model
674
  self.model.load_guides(self.selected_targets, self.organism, self.endonuclease)
675
  guides = self.model.get_guides()
 
 
 
 
676
  self.view.display_guides_in_table(guides)
677
  except Exception as e:
678
  show_error(self.settings, "Error refreshing guides display", str(e))
@@ -689,6 +719,9 @@ class ViewTargetsController:
689
  QApplication.processEvents()
690
 
691
  try:
 
 
 
692
  # Load data in chunks
693
  loading_dialog.set_message("Loading sequence data...", 30)
694
  QApplication.processEvents()
@@ -704,33 +737,41 @@ class ViewTargetsController:
704
  self.view.line_edit_start_location.setText(str(start))
705
  self.view.line_edit_stop_location.setText(str(end))
706
 
707
- # Filter guides efficiently
708
- loading_dialog.set_message("Filtering guides...", 60)
709
- QApplication.processEvents()
710
- position_guides = [g for g in self.model.guides
711
- if g.get('feature_id') == selected_text]
 
 
 
 
 
 
712
  self.view.display_guides_in_table(position_guides)
713
 
714
  else:
715
- # Regular gene-based search with optimized loading
716
  locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
717
- sequence_data = self.model.get_gene_sequence(locus_tag)
718
 
719
- if sequence_data:
720
- self.view.line_edit_start_location.setText(str(sequence_data['start']))
721
- self.view.line_edit_stop_location.setText(str(sequence_data['end']))
722
 
723
  loading_dialog.set_message("Updating display...", 80)
724
  QApplication.processEvents()
725
 
 
 
 
 
 
726
  gene_guides = [g for g in self.model.guides
727
- if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
728
  self.view.display_guides_in_table(gene_guides)
729
-
730
- # Refresh gene viewer
731
- loading_dialog.set_message("Refreshing viewer...", 90)
732
- QApplication.processEvents()
733
- self.refresh_gene_viewer()
734
 
735
  finally:
736
  loading_dialog.close()
@@ -781,19 +822,22 @@ class ViewTargetsController:
781
  print("Negative strand")
782
  # Convert sequence to complement for negative strand search
783
  print(f"Sequence: {sequence_upper}")
784
- complement_sequence = ''.join({'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G', 'K': 'M', 'Y': 'R', 'R': 'Y', 'M': 'K', 'S': 'S'}[base] for base in sequence_upper)
 
 
785
  print(f"Complement sequence: {complement_sequence}")
786
  target_sequence = guide_sequence.upper()
787
  print(f"Target sequence: {target_sequence}")
788
- target_sequence = target_sequence[::-1]
789
  print(f"Reversed target sequence: {target_sequence}")
790
  pos = complement_sequence.find(target_sequence)
791
  print(f"Position: {pos}")
 
792
  if pos != -1:
793
  color = QColor(255, 0, 0, 100) # Red for negative strand
794
  self.logger.debug(f"Found negative strand sequence at position {pos}")
795
 
796
- # Pass the original position but indicate negative strand
797
  self.view.dna_feature_viewer.sequence_viewer.highlight_sequence(
798
  pos,
799
  pos + len(guide_sequence) - 1,
@@ -1079,12 +1123,25 @@ class ViewTargetsController:
1079
  chrom = parts[0].split('chromosome')[1].strip()
1080
  start = int(parts[1].split('start:')[1].strip())
1081
  end = int(parts[2].split('end:')[1].strip())
 
 
1082
  sequence = self.model._get_sequence_for_position(chrom, start, end)
1083
 
1084
  if sequence:
1085
  # Get features for this region
1086
  features = self.model.get_features_for_region(chrom, start, end)
1087
- self.view.update_gene_viewer(sequence, features)
 
 
 
 
 
 
 
 
 
 
 
1088
  else:
1089
  # Regular gene-based search
1090
  locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
@@ -1094,8 +1151,22 @@ class ViewTargetsController:
1094
  if sequence_data and 'sequence' in sequence_data:
1095
  # Get features for this gene
1096
  features = self.model.get_features_for_gene(locus_tag)
1097
- self.view.update_gene_viewer(sequence_data['sequence'], features)
1098
- self.logger.debug(f"Updated gene viewer with sequence and {len(features)} features")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1099
  else:
1100
  self.logger.warning("No sequence data available")
1101
 
 
8
  from views.LoadingDialog import LoadingDialog
9
  from PyQt6.QtWidgets import QApplication
10
  from PyQt6.QtGui import QColor
11
+ import os
12
 
13
  class ViewTargetsController:
14
  def __init__(self, global_settings):
 
32
  self.view.push_button_change_location.clicked.connect(self.change_indices)
33
  self.view.push_button_reset_location.clicked.connect(self.reset_location)
34
  self.view.check_box_select_all.stateChanged.connect(self.select_all)
 
35
  self.view.gene_selected.connect(self.on_gene_selected)
36
 
37
  self.view.check_box_filter_5_prime_g_sequences.stateChanged.connect(self.refresh_guides_display)
 
42
  """Handle view exons only checkbox state change"""
43
  try:
44
  is_checked = self.view.check_box_view_exons_only.isChecked()
 
45
  self.model.set_view_exons_only(is_checked)
46
  self.refresh_gene_viewer()
47
  except Exception as e:
48
  self.logger.error(f"Error handling view exons change: {str(e)}")
49
 
50
+ def _clear_viewer_state(self):
51
+ """Clear all highlights and cursor states from the gene viewer"""
52
+ try:
53
+ if hasattr(self.view, 'dna_feature_viewer'):
54
+ # Clear sequence viewer highlights and cursor
55
+ self.view.dna_feature_viewer.sequence_viewer.clear_highlights()
56
+ for nuc in self.view.dna_feature_viewer.sequence_viewer.nucleotides:
57
+ nuc.show_cursor = False
58
+ nuc.update()
59
+ self.view.dna_feature_viewer.sequence_viewer.selection_active = False
60
+ self.view.dna_feature_viewer.sequence_viewer.selection_start = None
61
+ self.view.dna_feature_viewer.sequence_viewer.selection_end = None
62
+
63
+ # Clear insertion zone cursor
64
+ if hasattr(self.view.dna_feature_viewer, 'insertion_zone'):
65
+ if hasattr(self.view.dna_feature_viewer.insertion_zone, 'sequence_cursor'):
66
+ self.view.dna_feature_viewer.insertion_zone.sequence_cursor.hide()
67
+ self.view.dna_feature_viewer.insertion_zone.current_cursor_pos = None
68
+ self.view.dna_feature_viewer.insertion_zone.selection_start = None
69
+ self.view.dna_feature_viewer.insertion_zone.selection_end = None
70
+ except Exception as e:
71
+ self.logger.error(f"Error clearing viewer state: {str(e)}")
72
+
73
  def load_guides(self, selected_targets, organism, endonuclease, loading_dialog=None):
74
  try:
75
  self.organism = organism
 
84
  QApplication.processEvents()
85
 
86
  try:
87
+ # Clear any existing highlights and cursor
88
+ self._clear_viewer_state()
89
+
90
  loading_dialog.set_message("Loading guides...", 60)
91
  QApplication.processEvents()
92
 
 
117
  loading_dialog.set_message("Updating display...", 80)
118
  QApplication.processEvents()
119
 
120
+ # Get unique position names or gene IDs
121
+ unique_entries = set()
122
+ for target in selected_targets:
123
+ if 'feature_id' in target:
124
+ if "chromosome" in str(target['feature_id']):
125
+ unique_entries.add(target['feature_id'])
126
+ else:
127
+ locus_tag = target['feature_id']
128
+ gene_data = self.model.get_gene_data(locus_tag)
129
+ if gene_data and 'info' in gene_data:
130
+ gene_name = gene_data['info'].get('gene_name', '')
131
+ display_text = f"{locus_tag}: {gene_name}" if gene_name else locus_tag
132
+ unique_entries.add(display_text)
133
 
134
+ # Convert set to list for combo box
135
+ entries = list(unique_entries)
136
+ self.logger.debug(f"Found {len(entries)} unique entries")
137
+
138
+ if entries:
139
+ first_entry = entries[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
+ # Block signals during setup
142
+ self.view.combo_box_gene.blockSignals(True)
 
143
 
144
+ # Update combo box
145
  self.view.set_combo_box_gene(entries)
146
+ self.view.combo_box_gene.setCurrentIndex(0)
147
 
148
+ # Display guides for the first entry
149
+ if "chromosome" in first_entry and "start:" in first_entry:
150
+ position_guides = [g for g in guides if g.get('feature_id') == first_entry]
151
+ self.view.display_guides_in_table(position_guides)
152
+
153
+ # Parse position from the text
154
+ parts = first_entry.split(',')
155
+ chrom = parts[0].split('chromosome')[1].strip()
156
+ start = int(parts[1].split('start:')[1].strip())
157
+ end = int(parts[2].split('end:')[1].strip())
158
+
159
+ # Update location fields - add 1 to start for display
160
+ self.view.line_edit_start_location.setText(str(start + 1))
161
+ self.view.line_edit_stop_location.setText(str(end))
162
+
163
+ # Get sequence directly for position-based search
164
+ sequence = self.model._get_sequence_for_position(chrom, start, end)
165
+ if sequence:
166
+ # Single call to set_data
167
+ self.view.dna_feature_viewer.set_data(sequence, [], start)
168
+ else:
169
+ # Regular gene-based search
170
+ locus_tag = first_entry.split(': ')[0] if ': ' in first_entry else first_entry
171
+ gene_guides = [g for g in guides if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
172
+ self.view.display_guides_in_table(gene_guides)
173
+
174
+ # Get sequence data and features
175
+ sequence_data = self.model.get_gene_sequence(locus_tag)
176
+ if sequence_data:
177
+ # Update location fields - add 1 to start for display
178
+ self.view.line_edit_start_location.setText(str(sequence_data['start'] + 1))
179
+ self.view.line_edit_stop_location.setText(str(sequence_data['end']))
180
+
181
+ # Get features and update viewer in a single call
182
+ features = self.model.get_features_for_gene(locus_tag)
183
+ self.view.dna_feature_viewer.set_data(sequence_data['sequence'], features, sequence_data['start'])
184
 
185
+ # Now unblock signals after everything is set up
186
+ self.view.combo_box_gene.blockSignals(False)
187
 
188
  loading_dialog.set_progress(100)
189
  QApplication.processEvents()
 
199
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
200
  show_error(self.settings, "Error loading guides", str(e))
201
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  def _on_endonuclease_changed(self, new_endonuclease):
203
  try:
204
  if new_endonuclease != self.endonuclease:
 
218
  new_target['endonuclease'] = new_endonuclease
219
  updated_targets.append(new_target)
220
 
 
 
221
  # Update model with new targets
222
  self.model.load_guides(updated_targets, self.organism, new_endonuclease)
223
  guides = self.model.get_guides()
 
232
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
233
  show_error(self.settings, "Error", f"Could not change endonuclease: {str(e)}")
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  def perform_off_target_analysis(self):
236
  """Launch off-target analysis for selected guides"""
237
  try:
 
255
  self._handle_off_target_results
256
  )
257
 
258
+ # Get current annotation file
259
+ current_annotation_file = self.settings.get_current_annotation_file()
260
+ if not current_annotation_file:
261
+ QtWidgets.QMessageBox.warning(
262
+ self.view,
263
+ "No Annotation File",
264
+ "Please select an annotation file before performing off-target analysis."
265
+ )
266
+ return
267
+
268
+ # Verify annotation file exists
269
+ annotation_path = os.path.join(self.settings.get_db_path(), 'GBFF', current_annotation_file)
270
+ if not os.path.isfile(annotation_path):
271
+ # Try without GBFF subdirectory
272
+ annotation_path = os.path.join(self.settings.get_db_path(), current_annotation_file)
273
+ if not os.path.isfile(annotation_path):
274
+ QtWidgets.QMessageBox.warning(
275
+ self.view,
276
+ "Invalid Annotation File",
277
+ f"Could not find annotation file at {annotation_path}"
278
+ )
279
+ return
280
+
281
  # Set initial parameters based on current organism/endonuclease
282
  parameters = {
283
  'organism': self.organism,
284
  'endonuclease': self.endonuclease,
285
+ 'guides': selected_guides, # Pass the selected guides
286
+ 'annotation_file': current_annotation_file # Add annotation file
287
  }
288
 
289
  # Initialize analysis with parameters
 
473
  show_error(self.settings, "Error", f"Could not show scoring options: {str(e)}")
474
 
475
  def change_indices(self):
476
+ """Change the displayed sequence range"""
477
  try:
478
+ # Get new start and end positions
 
 
 
 
 
 
 
 
 
 
 
479
  try:
480
  new_start = int(self.view.line_edit_start_location.text())
481
  new_end = int(self.view.line_edit_stop_location.text())
 
512
  )
513
  return
514
 
515
+ # Get current gene/position
516
+ current_gene = self.view.combo_box_gene.currentText()
517
+
518
+ # Handle position-based search
519
  if "chromosome" in current_gene and "start:" in current_gene:
520
  # For position-based searches
521
  try:
 
523
  # Get full chromosome identifier instead of just the number
524
  chrom = parts[0].split('chromosome')[1].strip() # This will now keep the full identifier
525
 
526
+ # Get sequence for new range - subtract 1 from start for 0-based indexing
527
+ sequence = self.model._get_sequence_for_position(chrom, new_start - 1, new_end)
528
 
529
  if sequence:
530
+ # Update DNA viewer with sequence
531
+ self.view.dna_feature_viewer.set_data(sequence, [], new_start - 1)
532
+
533
+ # Update the line edits with new positions (keep display as 1-based)
534
  self.view.line_edit_start_location.setText(str(new_start))
535
  self.view.line_edit_stop_location.setText(str(new_end))
536
  else:
 
539
  except Exception as e:
540
  QMessageBox.warning(
541
  self.view,
542
+ "Sequence Error",
543
+ f"Could not get sequence for range {new_start}-{new_end} in chromosome {chrom}"
544
  )
 
545
  else:
546
+ # For gene-based searches
547
  locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
 
548
 
549
+ # Get gene data to get chromosome information
550
+ gene_data = self.model.get_gene_data(locus_tag)
 
 
 
 
 
551
 
552
+ if gene_data and 'info' in gene_data:
553
+ try:
554
+ # Get chromosome from gene data
555
+ chrom = gene_data['info']['chromosome']
556
+
557
+ # Get sequence directly using _get_sequence_for_position
558
+ sequence = self.model._get_sequence_for_position(chrom, new_start - 1, new_end)
559
+
560
+ if sequence:
561
+ # Update line edits (keep display as 1-based)
562
+ self.view.line_edit_start_location.setText(str(new_start))
563
+ self.view.line_edit_stop_location.setText(str(new_end))
564
+
565
+ # Get features for this gene
566
+ features = self.model.get_features_for_gene(locus_tag)
567
+
568
+ # Update DNA viewer with new sequence
569
+ self.view.dna_feature_viewer.set_data(sequence, features, new_start - 1)
570
+
571
+ # Update guides display for new range
572
+ gene_guides = [g for g in self.model.guides
573
+ if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower() and
574
+ new_start <= int(g['location'].split('-')[0]) <= new_end]
575
+ self.view.display_guides_in_table(gene_guides)
576
+ else:
577
+ raise ValueError("Could not get sequence for the specified range")
578
+
579
+ except ValueError as ve:
580
+ QMessageBox.warning(
581
+ self.view,
582
+ "Range Error",
583
+ f"Invalid range: {str(ve)}"
584
+ )
585
  else:
586
  QMessageBox.warning(
587
  self.view,
588
+ "Gene Error",
589
+ f"Could not get sequence data for gene {locus_tag}"
590
  )
591
+
 
592
  except Exception as e:
593
+ self.logger.error(f"Error changing indices: {str(e)}")
594
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
595
+ show_error(self.settings, "Error", f"Could not change sequence range: {str(e)}")
596
 
597
  def reset_location(self):
598
  """Reset gene viewer to the original sequence and location"""
 
612
  # Get sequence directly using model's method
613
  sequence = self.model._get_sequence_for_position(chrom, start, end)
614
  if sequence:
615
+ # Update DNA viewer with sequence
616
+ self.view.dna_feature_viewer.set_data(sequence, [], start)
617
 
618
+ # Update location fields - add 1 to start for display
619
  self.view.line_edit_start_location.setText(str(start + 1))
620
  self.view.line_edit_stop_location.setText(str(end))
621
+
622
+ # Update guides display
623
+ position_guides = [g for g in self.model.guides
624
+ if start <= int(g['location'].split('-')[0]) <= end]
625
+ self.view.display_guides_in_table(position_guides)
626
  else:
627
  raise ValueError("Could not get sequence for position")
628
 
 
635
  )
636
  return
637
  else:
638
+ # For gene-based searches
639
  locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
 
640
 
641
+ # Get original gene sequence
642
+ sequence_data = self.model.get_gene_sequence(locus_tag)
643
  if sequence_data:
644
+ # Get features for this gene
645
+ features = self.model.get_features_for_gene(locus_tag)
646
 
647
+ # Update DNA viewer with sequence and features
648
+ self.view.dna_feature_viewer.set_data(
649
+ sequence_data['sequence'],
650
+ features,
651
+ sequence_data['start']
652
+ )
653
+
654
+ # Update location fields - add 1 to start for display
655
  self.view.line_edit_start_location.setText(str(sequence_data['start'] + 1))
656
  self.view.line_edit_stop_location.setText(str(sequence_data['end']))
657
+
658
+ # Update guides display
659
+ gene_guides = [g for g in self.model.guides
660
+ if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
661
+ self.view.display_guides_in_table(gene_guides)
662
  else:
 
663
  QMessageBox.warning(
664
  self.view,
665
+ "Gene Error",
666
+ f"Could not get sequence data for gene {locus_tag}"
667
  )
668
+
 
669
  except Exception as e:
670
  self.logger.error(f"Error in reset_location: {str(e)}")
671
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
672
+ show_error(self.settings, "Reset Error", str(e))
673
 
674
  def select_all(self, state):
675
  try:
 
699
  # Get current guides from model
700
  self.model.load_guides(self.selected_targets, self.organism, self.endonuclease)
701
  guides = self.model.get_guides()
702
+
703
+ # Clear any existing highlights and cursor
704
+ self._clear_viewer_state()
705
+
706
  self.view.display_guides_in_table(guides)
707
  except Exception as e:
708
  show_error(self.settings, "Error refreshing guides display", str(e))
 
719
  QApplication.processEvents()
720
 
721
  try:
722
+ # Clear any existing highlights and cursor
723
+ self._clear_viewer_state()
724
+
725
  # Load data in chunks
726
  loading_dialog.set_message("Loading sequence data...", 30)
727
  QApplication.processEvents()
 
737
  self.view.line_edit_start_location.setText(str(start))
738
  self.view.line_edit_stop_location.setText(str(end))
739
 
740
+ # Get sequence directly for position-based search
741
+ sequence = self.model._get_sequence_for_position(chrom, start, end)
742
+ if sequence:
743
+ # Update DNA viewer with sequence
744
+ self.view.dna_feature_viewer.set_data(sequence, [], start)
745
+ self.logger.debug(f"Updated gene viewer with sequence of length {len(sequence)}")
746
+ else:
747
+ self.logger.error(f"Could not get sequence for position {start}-{end} in chromosome {chrom}")
748
+
749
+ # Filter guides for this position
750
+ position_guides = [g for g in self.model.guides if g.get('feature_id') == selected_text]
751
  self.view.display_guides_in_table(position_guides)
752
 
753
  else:
754
+ # Regular gene-based search
755
  locus_tag = selected_text.split(': ')[0] if ': ' in selected_text else selected_text
756
+ gene_data = self.model.get_gene_data(locus_tag)
757
 
758
+ if gene_data and 'sequence' in gene_data and 'info' in gene_data:
759
+ self.view.line_edit_start_location.setText(str(gene_data['info']['start'] + 1))
760
+ self.view.line_edit_stop_location.setText(str(gene_data['info']['end']))
761
 
762
  loading_dialog.set_message("Updating display...", 80)
763
  QApplication.processEvents()
764
 
765
+ # Get features and update viewer in a single call
766
+ features = self.model.get_features_for_gene(locus_tag)
767
+ self.view.dna_feature_viewer.set_data(gene_data['sequence'], features, gene_data['info']['start'])
768
+
769
+ # Filter guides for this gene
770
  gene_guides = [g for g in self.model.guides
771
+ if str(g.get('feature_id', '')).strip().lower() == locus_tag.lower()]
772
  self.view.display_guides_in_table(gene_guides)
773
+ else:
774
+ self.logger.warning(f"No valid gene data found for locus tag: {locus_tag}")
 
 
 
775
 
776
  finally:
777
  loading_dialog.close()
 
822
  print("Negative strand")
823
  # Convert sequence to complement for negative strand search
824
  print(f"Sequence: {sequence_upper}")
825
+ complement_sequence = ''.join({'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G',
826
+ 'K': 'M', 'Y': 'R', 'R': 'Y', 'M': 'K',
827
+ 'S': 'S'}[base] for base in sequence_upper)
828
  print(f"Complement sequence: {complement_sequence}")
829
  target_sequence = guide_sequence.upper()
830
  print(f"Target sequence: {target_sequence}")
831
+ target_sequence = target_sequence[::-1] # Reverse the sequence
832
  print(f"Reversed target sequence: {target_sequence}")
833
  pos = complement_sequence.find(target_sequence)
834
  print(f"Position: {pos}")
835
+
836
  if pos != -1:
837
  color = QColor(255, 0, 0, 100) # Red for negative strand
838
  self.logger.debug(f"Found negative strand sequence at position {pos}")
839
 
840
+ # For negative strand, use the position directly but indicate strand
841
  self.view.dna_feature_viewer.sequence_viewer.highlight_sequence(
842
  pos,
843
  pos + len(guide_sequence) - 1,
 
1123
  chrom = parts[0].split('chromosome')[1].strip()
1124
  start = int(parts[1].split('start:')[1].strip())
1125
  end = int(parts[2].split('end:')[1].strip())
1126
+
1127
+ self.logger.debug(f"Getting sequence for position: {chrom}:{start}-{end}")
1128
  sequence = self.model._get_sequence_for_position(chrom, start, end)
1129
 
1130
  if sequence:
1131
  # Get features for this region
1132
  features = self.model.get_features_for_region(chrom, start, end)
1133
+ self.logger.debug(f"Got sequence of length {len(sequence)} and {len(features)} features")
1134
+
1135
+ # Verify DNA viewer exists
1136
+ if not hasattr(self.view, 'dna_feature_viewer'):
1137
+ self.logger.error("DNA viewer not initialized!")
1138
+ return
1139
+
1140
+ # Update DNA viewer directly
1141
+ self.view.dna_feature_viewer.set_data(sequence, features, start)
1142
+ self.logger.debug("Updated DNA viewer with sequence data")
1143
+ else:
1144
+ self.logger.error("Failed to get sequence for position")
1145
  else:
1146
  # Regular gene-based search
1147
  locus_tag = current_gene.split(': ')[0] if ': ' in current_gene else current_gene
 
1151
  if sequence_data and 'sequence' in sequence_data:
1152
  # Get features for this gene
1153
  features = self.model.get_features_for_gene(locus_tag)
1154
+ sequence = sequence_data['sequence']
1155
+ start_pos = sequence_data['start']
1156
+
1157
+ self.logger.debug(f"Got sequence of length {len(sequence)}")
1158
+ self.logger.debug(f"First 50 chars: {sequence[:50]}")
1159
+ self.logger.debug(f"Start position: {start_pos}")
1160
+ self.logger.debug(f"Number of features: {len(features)}")
1161
+
1162
+ # Verify DNA viewer exists
1163
+ if not hasattr(self.view, 'dna_feature_viewer'):
1164
+ self.logger.error("DNA viewer not initialized!")
1165
+ return
1166
+
1167
+ # Update DNA viewer directly
1168
+ self.view.dna_feature_viewer.set_data(sequence, features, start_pos)
1169
+ self.logger.debug("Updated DNA viewer with sequence data")
1170
  else:
1171
  self.logger.warning("No sequence data available")
1172
 
src/main.py CHANGED
@@ -2,42 +2,72 @@ import sys
2
  import os
3
  import platform
4
  from PyQt6.QtWidgets import QApplication
5
- from PyQt6.QtCore import Qt
6
  from models.GlobalSettings import GlobalSettings
7
  from utils.ui import show_error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  def get_app_directory():
10
  """Determine the application root directory based on whether we're frozen or not"""
11
  if hasattr(sys, 'frozen'):
12
  if platform.system() == 'Darwin': # macOS
13
- # Get the path to the executable inside the .app bundle
14
  bundle_dir = os.path.abspath(os.path.dirname(sys.executable))
15
- # Navigate up to Contents directory and set Resources as app_dir
16
  return os.path.join(os.path.dirname(os.path.dirname(bundle_dir)), 'Contents', 'Resources')
17
  else:
18
- # For other platforms when frozen
19
  return os.path.dirname(sys.executable)
20
  else:
21
- # Development environment - go up one level from src directory
22
  return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
 
24
  def main():
25
- RESTART_CODE = 1000 # Define restart code constant
 
 
 
26
 
27
  while True:
28
  app = QApplication(sys.argv)
29
  app.setOrganizationName("TrinhLab-UTK")
30
  app.setApplicationName("CASPER")
31
 
 
32
  if hasattr(Qt.ApplicationAttribute, 'AA_UseHighDpiPixmaps'):
33
  app.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps)
 
34
 
 
 
 
35
  # Get the application directory
36
  app_dir_path = get_app_directory()
37
 
38
  try:
39
  global_settings = GlobalSettings(app_dir_path)
40
 
 
41
  from controllers.MainWindowController import MainWindowController
42
  main_window_controller = MainWindowController(global_settings)
43
  global_settings.set_main_window(main_window_controller)
 
2
  import os
3
  import platform
4
  from PyQt6.QtWidgets import QApplication
5
+ from PyQt6.QtCore import Qt, QCoreApplication
6
  from models.GlobalSettings import GlobalSettings
7
  from utils.ui import show_error
8
+ import importlib
9
+ import concurrent.futures
10
+
11
+ def preload_modules():
12
+ """Preload commonly used modules in parallel"""
13
+ modules_to_load = [
14
+ 'PyQt6.QtWidgets',
15
+ 'PyQt6.QtCore',
16
+ 'PyQt6.QtGui',
17
+ 'controllers.MainWindowController',
18
+ 'views.MainWindowView',
19
+ 'models.MainWindowModel',
20
+ 'models.DatabaseManager',
21
+ 'models.ConfigManager'
22
+ ]
23
+
24
+ def import_module(module_name):
25
+ try:
26
+ importlib.import_module(module_name)
27
+ return True, module_name
28
+ except Exception as e:
29
+ return False, f"Failed to load {module_name}: {str(e)}"
30
+
31
+ with concurrent.futures.ThreadPoolExecutor() as executor:
32
+ executor.map(import_module, modules_to_load)
33
 
34
  def get_app_directory():
35
  """Determine the application root directory based on whether we're frozen or not"""
36
  if hasattr(sys, 'frozen'):
37
  if platform.system() == 'Darwin': # macOS
 
38
  bundle_dir = os.path.abspath(os.path.dirname(sys.executable))
 
39
  return os.path.join(os.path.dirname(os.path.dirname(bundle_dir)), 'Contents', 'Resources')
40
  else:
 
41
  return os.path.dirname(sys.executable)
42
  else:
 
43
  return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
44
 
45
  def main():
46
+ RESTART_CODE = 1000
47
+
48
+ # Preload modules in parallel
49
+ preload_modules()
50
 
51
  while True:
52
  app = QApplication(sys.argv)
53
  app.setOrganizationName("TrinhLab-UTK")
54
  app.setApplicationName("CASPER")
55
 
56
+ # Enable high DPI scaling
57
  if hasattr(Qt.ApplicationAttribute, 'AA_UseHighDpiPixmaps'):
58
  app.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps)
59
+ app.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling, True)
60
 
61
+ # Enable Qt's built-in caching mechanisms
62
+ QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_ShareOpenGLContexts)
63
+
64
  # Get the application directory
65
  app_dir_path = get_app_directory()
66
 
67
  try:
68
  global_settings = GlobalSettings(app_dir_path)
69
 
70
+ # Import here after preloading
71
  from controllers.MainWindowController import MainWindowController
72
  main_window_controller = MainWindowController(global_settings)
73
  global_settings.set_main_window(main_window_controller)
src/models/AnnotationParser.py CHANGED
@@ -36,7 +36,6 @@ class AnnotationParser:
36
 
37
  if self.annotation_file_name != file_path:
38
  self.annotation_file_name = file_path
39
- self.logger.debug(f"Set annotation file to: {file_path}")
40
 
41
  # Set index file path
42
  self.index_file = f"{file_path}.index"
@@ -90,10 +89,18 @@ class AnnotationParser:
90
 
91
  # Only process features with valid locus tags
92
  if locus_tag and locus_tag.lower() != "n/a":
 
 
 
 
 
 
 
93
  # Handle joined locations
94
  if isinstance(feature.location, Bio.SeqFeature.CompoundLocation):
95
- if locus_tag == "CAALFM_C304810CA":
96
  print(f"Feature location: {feature.location}")
 
97
  # Get all parts of the joined location
98
  parts = feature.location.parts
99
  # Find min start and max end across all parts
@@ -121,10 +128,6 @@ class AnnotationParser:
121
  strand = '+' if feature.location.strand == 1 else '-'
122
  full_location = f"{start}..{end}({strand})"
123
 
124
- # Get description first since we might need it for the name
125
- description = feature.qualifiers.get('product',
126
- feature.qualifiers.get('note', ['N/A']))[0]
127
-
128
  # Get gene name, use description if gene name is N/A
129
  gene_name = feature.qualifiers.get('gene', ['N/A'])[0]
130
  if gene_name == 'N/A':
@@ -142,35 +145,40 @@ class AnnotationParser:
142
  'end': end
143
  }
144
 
145
- # Update index based on priority
146
  if locus_tag in index_data['locus_tags']:
147
  existing_entry = index_data['locus_tags'][locus_tag]
148
  existing_priority = feature_priority[existing_entry['feature_type']]
149
  current_priority = feature_priority[feature.type]
150
 
 
 
 
 
151
  if current_priority >= existing_priority:
152
- # Keep the RNA/higher priority feature type
153
- merged_entry = existing_entry.copy()
154
  merged_entry['feature_type'] = feature.type
155
-
156
- # Update other fields only if they're not 'N/A'
157
- if feature_entry['gene_name'] != 'N/A':
158
- merged_entry['gene_name'] = feature_entry['gene_name']
159
- if feature_entry['description'] != 'N/A':
160
- merged_entry['description'] = feature_entry['description']
161
- # If gene name is N/A, use the new description
162
- if merged_entry['gene_name'] == 'N/A':
163
- merged_entry['gene_name'] = feature_entry['description']
164
-
165
- # Always update location information
 
 
 
166
  merged_entry.update({
167
  'location': feature_entry['location'],
168
  'full_location': feature_entry['full_location'],
169
  'start': feature_entry['start'],
170
  'end': feature_entry['end']
171
  })
172
-
173
- index_data['locus_tags'][locus_tag] = merged_entry
174
  else:
175
  # New entry
176
  index_data['locus_tags'][locus_tag] = feature_entry
@@ -219,17 +227,17 @@ class AnnotationParser:
219
  self.logger.debug(f"Searching in annotation file: {self.annotation_file_name}")
220
  results_list = []
221
 
222
- # Convert queries to lowercase set for faster lookup
223
  queries = {q.lower() for q in queries}
224
- self.logger.debug(f"Search queries: {queries}")
225
 
226
  # Search through index
227
  if hasattr(self, '_index') and 'locus_tags' in self._index:
228
  for locus_tag, feature_entry in self._index['locus_tags'].items():
 
229
  searchable_text = ' '.join([
230
  feature_entry.get('gene_name', '').lower(),
231
  locus_tag.lower(),
232
- feature_entry.get('description', '').lower()
 
233
  ])
234
 
235
  # Check if any query matches
@@ -319,7 +327,6 @@ class AnnotationParser:
319
  def _get_sequence_for_gene(self, gene_info):
320
  """Get sequence for a gene from the GenBank file"""
321
  try:
322
- self.logger.debug(f"Getting sequence for gene info: {gene_info} in _get_sequence_for_gene")
323
  # Parse the GenBank file and find the right record
324
  for record in SeqIO.parse(self.annotation_file_name, "genbank"):
325
  if record.id == gene_info['chromosome']: # Use full chromosome name
@@ -330,11 +337,39 @@ class AnnotationParser:
330
  start = max(0, gene_info['start'] - padding)
331
  end = min(len(sequence), gene_info['end'] + padding)
332
  padded_sequence = sequence[start:end]
333
-
334
- self.logger.debug(f"Padded sequence: {padded_sequence}")
335
  return padded_sequence
336
  return None
337
 
338
  except Exception as e:
339
  self.logger.error(f"Error getting sequence for gene: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  return None
 
36
 
37
  if self.annotation_file_name != file_path:
38
  self.annotation_file_name = file_path
 
39
 
40
  # Set index file path
41
  self.index_file = f"{file_path}.index"
 
89
 
90
  # Only process features with valid locus tags
91
  if locus_tag and locus_tag.lower() != "n/a":
92
+ # Get description, use product as fallback
93
+ description = feature.qualifiers.get('description', ['N/A'])[0]
94
+ if description == 'N/A' or not description:
95
+ description = feature.qualifiers.get('product', ['N/A'])[0]
96
+ if locus_tag == "BN896_RS00070":
97
+ print(f"Feature description: {description}")
98
+
99
  # Handle joined locations
100
  if isinstance(feature.location, Bio.SeqFeature.CompoundLocation):
101
+ if locus_tag == "BN896_RS00070":
102
  print(f"Feature location: {feature.location}")
103
+ # print(f"Feature product:" )
104
  # Get all parts of the joined location
105
  parts = feature.location.parts
106
  # Find min start and max end across all parts
 
128
  strand = '+' if feature.location.strand == 1 else '-'
129
  full_location = f"{start}..{end}({strand})"
130
 
 
 
 
 
131
  # Get gene name, use description if gene name is N/A
132
  gene_name = feature.qualifiers.get('gene', ['N/A'])[0]
133
  if gene_name == 'N/A':
 
145
  'end': end
146
  }
147
 
148
+ # Update index based on modified priority logic
149
  if locus_tag in index_data['locus_tags']:
150
  existing_entry = index_data['locus_tags'][locus_tag]
151
  existing_priority = feature_priority[existing_entry['feature_type']]
152
  current_priority = feature_priority[feature.type]
153
 
154
+ # Always create a merged entry
155
+ merged_entry = existing_entry.copy()
156
+
157
+ # Update feature type only if priority is higher
158
  if current_priority >= existing_priority:
 
 
159
  merged_entry['feature_type'] = feature.type
160
+
161
+ # Always update description if new one is not N/A
162
+ if feature_entry['description'] != 'N/A':
163
+ merged_entry['description'] = feature_entry['description']
164
+ # If gene name is N/A, use the new description
165
+ if merged_entry['gene_name'] == 'N/A':
166
+ merged_entry['gene_name'] = feature_entry['description']
167
+
168
+ # Update other fields if they're not 'N/A'
169
+ if feature_entry['gene_name'] != 'N/A':
170
+ merged_entry['gene_name'] = feature_entry['gene_name']
171
+
172
+ # Always update location information if priority is higher
173
+ if current_priority >= existing_priority:
174
  merged_entry.update({
175
  'location': feature_entry['location'],
176
  'full_location': feature_entry['full_location'],
177
  'start': feature_entry['start'],
178
  'end': feature_entry['end']
179
  })
180
+
181
+ index_data['locus_tags'][locus_tag] = merged_entry
182
  else:
183
  # New entry
184
  index_data['locus_tags'][locus_tag] = feature_entry
 
227
  self.logger.debug(f"Searching in annotation file: {self.annotation_file_name}")
228
  results_list = []
229
 
 
230
  queries = {q.lower() for q in queries}
 
231
 
232
  # Search through index
233
  if hasattr(self, '_index') and 'locus_tags' in self._index:
234
  for locus_tag, feature_entry in self._index['locus_tags'].items():
235
+ # Create searchable text including feature type
236
  searchable_text = ' '.join([
237
  feature_entry.get('gene_name', '').lower(),
238
  locus_tag.lower(),
239
+ feature_entry.get('description', '').lower(),
240
+ feature_entry.get('feature_type', '').lower() # Add feature type to searchable text
241
  ])
242
 
243
  # Check if any query matches
 
327
  def _get_sequence_for_gene(self, gene_info):
328
  """Get sequence for a gene from the GenBank file"""
329
  try:
 
330
  # Parse the GenBank file and find the right record
331
  for record in SeqIO.parse(self.annotation_file_name, "genbank"):
332
  if record.id == gene_info['chromosome']: # Use full chromosome name
 
337
  start = max(0, gene_info['start'] - padding)
338
  end = min(len(sequence), gene_info['end'] + padding)
339
  padded_sequence = sequence[start:end]
 
 
340
  return padded_sequence
341
  return None
342
 
343
  except Exception as e:
344
  self.logger.error(f"Error getting sequence for gene: {str(e)}")
345
+ return None
346
+
347
+ def _get_sequence_for_position(self, chrom, start, end):
348
+ """Get sequence for a specific position range from the GenBank file
349
+
350
+ Args:
351
+ chrom (str): Chromosome identifier
352
+ start (int): Start position (0-based)
353
+ end (int): End position
354
+
355
+ Returns:
356
+ str: The sequence for the specified range with padding, or None if not found
357
+ """
358
+ try:
359
+ self.logger.debug(f"Getting sequence for position {chrom}:{start}-{end}")
360
+ # Parse the GenBank file and find the right record
361
+ for record in SeqIO.parse(self.annotation_file_name, "genbank"):
362
+ if record.id == chrom: # Use full chromosome name
363
+ sequence = str(record.seq)
364
+
365
+ # Get sequence with padding
366
+ padding = 30
367
+ padded_start = max(0, start - padding)
368
+ padded_end = min(len(sequence), end + padding)
369
+ padded_sequence = sequence[padded_start:padded_end]
370
+ return padded_sequence
371
+ return None
372
+
373
+ except Exception as e:
374
+ self.logger.error(f"Error getting sequence for position: {str(e)}")
375
  return None
src/models/BaseModel.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from models.AnnotationParser import AnnotationParser
2
+ import os
3
+
4
+ class BaseModel:
5
+ def __init__(self, global_settings):
6
+ self.global_settings = global_settings
7
+ self.logger = global_settings.get_logger()
8
+
9
+ # Initialize the annotation parser
10
+ self._initialize_annotation_parser()
11
+
12
+ # Connect to annotation file changes
13
+ self.global_settings.annotation_file_changed.connect(self._on_annotation_file_changed)
14
+
15
+ def _initialize_annotation_parser(self) -> None:
16
+ """Initialize or get existing annotation parser from global settings"""
17
+ try:
18
+ # Try to get existing parser from global settings
19
+ if not hasattr(self.global_settings, 'annotation_parser'):
20
+ # Create new parser if none exists
21
+ annotation_file = self.global_settings.get_current_annotation_file()
22
+ annotation_path = os.path.join(
23
+ self.global_settings.get_db_path(),
24
+ 'GBFF',
25
+ annotation_file
26
+ )
27
+ self.global_settings.annotation_parser = AnnotationParser(self.global_settings)
28
+ self.global_settings.annotation_parser.set_annotation_file(annotation_path)
29
+ self.logger.debug("Created new annotation parser")
30
+
31
+ self.annotation_parser = self.global_settings.annotation_parser
32
+ self.annotation_path = self.annotation_parser.annotation_file_name
33
+
34
+ except Exception as e:
35
+ self.logger.error(f"Error initializing annotation parser: {str(e)}")
36
+ raise
37
+
38
+ def _on_annotation_file_changed(self, new_annotation_file):
39
+ """Handle annotation file changes"""
40
+ try:
41
+ self.logger.debug(f"Clearing caches for new annotation file: {new_annotation_file}")
42
+ self._clear_caches()
43
+ self._initialize_annotation_parser()
44
+ except Exception as e:
45
+ self.logger.error(f"Error handling annotation file change: {str(e)}")
46
+
47
+ def _clear_caches(self):
48
+ """Clear model-specific caches. Override in subclasses."""
49
+ pass
50
+
51
+ def cleanup(self):
52
+ """Cleanup resources. Override in subclasses if needed."""
53
+ try:
54
+ self.global_settings.annotation_file_changed.disconnect(self._on_annotation_file_changed)
55
+ self.logger.debug("Disconnected from annotation file changes")
56
+ self._clear_caches()
57
+ except Exception as e:
58
+ self.logger.error(f"Error in cleanup: {str(e)}")
src/models/ConfigManager.py CHANGED
@@ -94,24 +94,71 @@ class ConfigManager(QObject):
94
  self.env_file_created.emit()
95
 
96
  def write_to_env(self, key, value):
97
- with open(self.env_path, 'r') as f:
98
- lines = f.readlines()
 
 
 
 
 
 
99
 
100
- with open(self.env_path, 'w') as f:
 
 
101
  for line in lines:
102
- if line.startswith(f'{key}='):
103
- f.write(f'{key}="{value}"\n') # Always use double quotes
 
104
  else:
105
- f.write(line)
106
- self.logger.info(f"Updated {key} in .env file")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  def get_env_value(self, key, default=None):
109
  return os.getenv(key, default)
110
 
111
  def set_env_value(self, key, value):
112
- self.write_to_env(key, value)
113
- os.environ[key] = value
114
- self.logger.info(f"Set environment variable: {key}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  def get_config_value(self, key, default=None):
117
  keys = key.split('.')
 
94
  self.env_file_created.emit()
95
 
96
  def write_to_env(self, key, value):
97
+ """Write or update a key-value pair in the .env file"""
98
+ try:
99
+ # Read all lines
100
+ if os.path.exists(self.env_path):
101
+ with open(self.env_path, 'r') as f:
102
+ lines = f.readlines()
103
+ else:
104
+ lines = []
105
 
106
+ # Find if key exists
107
+ key_exists = False
108
+ new_lines = []
109
  for line in lines:
110
+ if line.strip().startswith(f'{key}='):
111
+ new_lines.append(f'{key}="{value}"\n') # Always use double quotes
112
+ key_exists = True
113
  else:
114
+ new_lines.append(line)
115
+
116
+ # If key doesn't exist, append it
117
+ if not key_exists:
118
+ new_lines.append(f'{key}="{value}"\n')
119
+
120
+ # Write back to file
121
+ with open(self.env_path, 'w') as f:
122
+ f.writelines(new_lines)
123
+
124
+ # Update environment variable in memory
125
+ os.environ[key] = value
126
+
127
+ self.logger.info(f"Updated {key}={value} in .env file")
128
+
129
+ # Verify the write
130
+ with open(self.env_path, 'r') as f:
131
+ content = f.read()
132
+ if f'{key}="{value}"' not in content:
133
+ self.logger.error(f"Failed to verify {key}={value} in .env file")
134
+ raise Exception("Failed to verify environment variable update")
135
+
136
+ except Exception as e:
137
+ self.logger.error(f"Error writing to .env file: {str(e)}")
138
+ raise
139
 
140
  def get_env_value(self, key, default=None):
141
  return os.getenv(key, default)
142
 
143
  def set_env_value(self, key, value):
144
+ """Set and save an environment variable"""
145
+ try:
146
+ # Write to .env file first
147
+ self.write_to_env(key, value)
148
+
149
+ # Update runtime environment
150
+ os.environ[key] = value
151
+
152
+ # Verify the update
153
+ if os.getenv(key) != value:
154
+ self.logger.error(f"Failed to verify environment variable update for {key}")
155
+ raise Exception(f"Environment variable verification failed for {key}")
156
+
157
+ self.logger.info(f"Successfully set environment variable: {key}={value}")
158
+
159
+ except Exception as e:
160
+ self.logger.error(f"Error setting environment variable {key}: {str(e)}")
161
+ raise
162
 
163
  def get_config_value(self, key, default=None):
164
  keys = key.split('.')
src/models/DatabaseManager.py CHANGED
@@ -5,6 +5,7 @@ from collections import Counter
5
  import statistics
6
  from enum import Enum
7
  from typing import Set, Dict, List, Tuple
 
8
 
9
  class FileChangeType(Enum):
10
  CSPR_ADDED = "cspr_added"
@@ -23,86 +24,175 @@ class DatabaseManager(QObject):
23
  self.logger = logger
24
  self.config_manager = config_manager
25
  self.db_path = None
 
 
 
 
 
 
 
 
 
 
26
  self.file_watcher = QFileSystemWatcher()
27
  self.file_watcher.directoryChanged.connect(self._on_directory_changed)
 
 
28
  self.load_database_path()
29
  self._update_watched_directory()
30
 
31
- self._last_cspr_files: Set[str] = set(self._get_cspr_files())
32
- self._last_gbff_files: Set[str] = set(self._get_gbff_files())
 
 
 
 
 
 
33
 
34
  def load_database_path(self):
35
- """Load the database path from .env file or set default if empty."""
36
- db_path = self.config_manager.get_env_value('CSPR_DB', '')
37
- # Remove both single and double quotes if present
38
- db_path = db_path.strip("'\"")
39
- if not db_path:
40
- db_path = self.get_default_database_path()
41
- self.save_db_path(db_path)
42
- self.db_path = db_path
43
- return self.db_path
 
 
 
 
 
 
 
 
 
44
 
45
  def validate_db_path(self, path):
46
- """Validate that the given path exists and contains CSPR files"""
47
- self.logger.debug(f"Validating DB path: {path}")
48
-
49
- if not os.path.isdir(path):
50
- self.logger.debug(f"Path is not a directory: {path}")
51
- return False, "The selected path is not a directory."
 
 
 
 
52
 
53
- cspr_files = self._get_cspr_files()
54
- if not cspr_files:
55
- self.logger.debug(f"Path {path} does not contain CSPR files")
56
- return False, "The selected directory does not contain any CSPR files."
57
 
58
- self.logger.debug(f"Path {path} is valid and contains {len(cspr_files)} CSPR files")
59
- return True, f"Valid database path with {len(cspr_files)} CSPR files"
 
 
 
60
 
61
  def save_db_path(self, path):
62
  """Set and save the database path."""
63
- if not path:
64
- self.logger.warning("Attempting to save an empty database path")
65
- return False, "Empty database path is not allowed."
66
-
67
- # Ensure the path is a string and properly quoted
68
- path = str(path).strip("'\"")
69
-
70
- # Validate the database path
71
- is_valid, message = self.validate_db_path(path)
72
- if not is_valid:
73
- self.logger.warning(f"Invalid database path: {path}")
74
- self.db_validation_changed.emit(False, message)
75
- self.db_path = path
76
- self.config_manager.set_env_value('CSPR_DB', path)
77
- self._update_watched_directory()
78
- return False, message
79
-
80
- # Set the db_path attribute
81
- self.db_path = path
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  try:
84
- self.config_manager.set_env_value('CSPR_DB', path)
85
- self.logger.info(f"Database path set and saved: {path}")
86
- self.db_validation_changed.emit(True, "Database path saved successfully.")
87
- self._update_watched_directory()
88
- return True, "Database path saved successfully."
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  except Exception as e:
90
- error_message = f"Error saving database path: {str(e)}"
91
- self.logger.error(error_message)
92
- self.db_validation_changed.emit(False, error_message)
93
- return False, error_message
 
 
 
 
 
 
 
94
 
95
  def get_db_path(self):
96
  return self.db_path
97
 
98
  def ensure_db_path_exists(self):
99
  """Ensure that the database path exists, creating it if necessary."""
100
- if not os.path.exists(self.db_path):
 
101
  try:
102
- os.makedirs(self.db_path)
103
- self.logger.info(f"Created database directory: {self.db_path}")
104
  except Exception as e:
105
- self.logger.error(f"Failed to create database directory: {self.db_path}. Error: {str(e)}")
106
  raise
107
 
108
  def get_default_database_path(self):
@@ -190,24 +280,36 @@ class DatabaseManager(QObject):
190
  try:
191
  self.logger.debug(f"Detected change in directory: {path}")
192
 
 
 
 
 
 
 
 
 
 
 
 
193
  # Re-validate the path
194
  is_valid, message = self.validate_db_path(self.db_path)
195
 
196
  # Detect specific changes
197
  changes = self._detect_file_changes()
198
 
199
- # Always emit validation signal on directory change
 
 
 
 
200
  self.db_validation_changed.emit(is_valid, message)
 
201
 
202
- if changes: # Only emit change signals if there are actual changes
 
203
  self.logger.debug(f"Detected file changes: {changes}")
204
  self.db_files_changed.emit(changes)
205
-
206
- # Always emit combined state signal
207
- self.db_state_changed.emit(is_valid, message, changes or {})
208
 
209
- self.logger.info(f"Database state updated - Valid: {is_valid}, Changes: {changes}")
210
-
211
  except Exception as e:
212
  self.logger.error(f"Error handling directory change: {str(e)}")
213
 
@@ -229,23 +331,24 @@ class DatabaseManager(QObject):
229
  if f.endswith('.gbff')]
230
 
231
  def check_db_state(self):
232
- """Check the current state of the database and emit signals if needed."""
233
- self.logger.debug("Checking database state")
234
- if not self.db_path:
235
- self.load_database_path()
236
-
237
- # Get validation state
238
- is_valid, message = self.validate_db_path(self.db_path)
239
-
240
- # Detect any changes since last check
241
- changes = self._detect_file_changes()
242
-
243
- # Emit signals
244
- self.db_validation_changed.emit(is_valid, message)
245
- if changes:
246
- self.db_files_changed.emit(changes)
247
 
248
- self.logger.info(f"Database state checked - Valid: {is_valid}, Changes: {changes}")
 
 
 
 
 
 
 
 
 
 
249
 
250
  def get_organisms_and_endos(self):
251
  """Get mapping of organisms to their endonucleases and files"""
@@ -510,3 +613,105 @@ class DatabaseManager(QObject):
510
  except Exception as e:
511
  self.logger.error(f"Error calculating statistics: {str(e)}")
512
  raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import statistics
6
  from enum import Enum
7
  from typing import Set, Dict, List, Tuple
8
+ import glob
9
 
10
  class FileChangeType(Enum):
11
  CSPR_ADDED = "cspr_added"
 
24
  self.logger = logger
25
  self.config_manager = config_manager
26
  self.db_path = None
27
+ self.pending_db_path = None
28
+ self.is_changing_directory = False
29
+ self._is_validating = False # Add flag to prevent recursion
30
+
31
+ # Initialize last known states before file watcher
32
+ self._last_cspr_files = set()
33
+ self._last_gbff_files = set()
34
+ self._last_files = {} # Track files in each watched directory
35
+
36
+ # Initialize file watcher
37
  self.file_watcher = QFileSystemWatcher()
38
  self.file_watcher.directoryChanged.connect(self._on_directory_changed)
39
+
40
+ # Load database path and update states
41
  self.load_database_path()
42
  self._update_watched_directory()
43
 
44
+ # Update last known states after path is loaded
45
+ self._last_cspr_files = set(self._get_cspr_files())
46
+ self._last_gbff_files = set(self._get_gbff_files())
47
+ if self.db_path:
48
+ self._last_files[self.db_path] = set(os.listdir(self.db_path))
49
+ gbff_path = os.path.join(self.db_path, 'GBFF')
50
+ if os.path.exists(gbff_path):
51
+ self._last_files[gbff_path] = set(os.listdir(gbff_path))
52
 
53
  def load_database_path(self):
54
+ """Load the database path from .env file."""
55
+ try:
56
+ db_path = self.config_manager.get_env_value('CSPR_DB', '')
57
+ # Remove both single and double quotes if present
58
+ db_path = db_path.strip("'\"")
59
+
60
+ # Only set default if no path exists at all
61
+ if not db_path and self.config_manager.get_env_value('FIRST_TIME_START', 'TRUE').upper() == 'TRUE':
62
+ db_path = self.get_default_database_path()
63
+ self.save_db_path(db_path)
64
+
65
+ self.db_path = db_path
66
+ self.logger.debug(f"Database path loaded from .env: {self.db_path}")
67
+ return self.db_path
68
+
69
+ except Exception as e:
70
+ self.logger.error(f"Error loading database path: {str(e)}")
71
+ return self.get_default_database_path()
72
 
73
  def validate_db_path(self, path):
74
+ """
75
+ Validate the database path without modifying it
76
+ Returns (is_valid, message)
77
+ """
78
+ try:
79
+ if not path:
80
+ return False, "No directory selected"
81
+
82
+ if not os.path.exists(path):
83
+ return False, "The selected directory does not exist."
84
 
85
+ # Check for CSPR files
86
+ cspr_files = glob.glob(os.path.join(path, "*.cspr"))
87
+ if not cspr_files:
88
+ return False, "No CSPR files found"
89
 
90
+ return True, "Valid database directory"
91
+
92
+ except Exception as e:
93
+ self.logger.error(f"Error validating database path: {str(e)}")
94
+ return False, str(e)
95
 
96
  def save_db_path(self, path):
97
  """Set and save the database path."""
98
+ if self._is_validating: # Prevent recursive validation
99
+ return True, "Operation in progress"
100
+
101
+ try:
102
+ self._is_validating = True
103
+
104
+ if not path:
105
+ self.logger.warning("Attempting to save an empty database path")
106
+ return False, "Empty database path is not allowed."
 
 
 
 
 
 
 
 
 
 
107
 
108
+ path = str(path).strip("'\"")
109
+ is_new_genome = self._is_new_genome_context()
110
+
111
+ # Validate the database path
112
+ is_valid, message = self.validate_db_path(path)
113
+
114
+ # Log the current state
115
+ self.logger.debug(f"Saving DB path - Current state: db_path={self.db_path}, pending={self.pending_db_path}, "
116
+ f"is_changing={self.is_changing_directory}, is_new_genome={is_new_genome}")
117
+
118
+ # If we're in new genome context or path change context, store as pending path
119
+ if is_new_genome or is_valid:
120
+ self.logger.debug(f"Storing pending database path: {path}")
121
+ self.pending_db_path = path
122
+ self.is_changing_directory = True
123
+
124
+ # Create directory if needed
125
+ if not os.path.exists(path):
126
+ try:
127
+ os.makedirs(path)
128
+ self.logger.info(f"Created pending directory: {path}")
129
+ except Exception as e:
130
+ self.logger.error(f"Error creating pending directory: {str(e)}")
131
+
132
+ # If the path is valid, finalize the change immediately
133
+ if is_valid:
134
+ success, finalize_message = self.finalize_directory_change()
135
+ if not success:
136
+ self.logger.error(f"Failed to finalize directory change: {finalize_message}")
137
+ return False, finalize_message
138
+ return True, finalize_message
139
+
140
+ return True, "Path stored for new genome creation"
141
+
142
+ # For invalid paths
143
+ if not is_valid:
144
+ self.db_validation_changed.emit(False, message)
145
+ return False, message
146
+
147
+ finally:
148
+ self._is_validating = False
149
+
150
+ def _is_new_genome_context(self):
151
+ """Check if the path change is happening in new genome context"""
152
  try:
153
+ import inspect
154
+ stack = inspect.stack()
155
+
156
+ # Check for NCBI context as well
157
+ is_new_genome = any('NewGenome' in frame.filename or 'NCBI' in frame.filename for frame in stack)
158
+ is_path_change = any('MainWindow' in frame.filename and 'change_database_directory' in frame.function
159
+ for frame in stack)
160
+
161
+ # Consider it a new genome context if either:
162
+ # 1. We're in new genome/NCBI context and actively changing directory
163
+ # 2. We're in the path change process
164
+ is_active_change = (is_new_genome and self.is_changing_directory) or is_path_change
165
+
166
+ self.logger.debug(f"Context check - New Genome: {is_new_genome}, Path Change: {is_path_change}, "
167
+ f"Active Change: {is_active_change}, Is Changing Directory: {self.is_changing_directory}")
168
+
169
+ return is_active_change
170
+
171
  except Exception as e:
172
+ self.logger.error(f"Error in _is_new_genome_context: {str(e)}")
173
+ return False
174
+
175
+ def get_active_db_path(self):
176
+ """Get the appropriate database path based on context"""
177
+ if self.pending_db_path and self.is_changing_directory:
178
+ self.logger.debug(f"Using pending database path: {self.pending_db_path}")
179
+ return self.pending_db_path
180
+
181
+ self.logger.debug(f"Using current database path: {self.db_path}")
182
+ return self.db_path
183
 
184
  def get_db_path(self):
185
  return self.db_path
186
 
187
  def ensure_db_path_exists(self):
188
  """Ensure that the database path exists, creating it if necessary."""
189
+ path_to_check = self.get_active_db_path() # Use active path instead of db_path
190
+ if not os.path.exists(path_to_check):
191
  try:
192
+ os.makedirs(path_to_check)
193
+ self.logger.info(f"Created database directory: {path_to_check}")
194
  except Exception as e:
195
+ self.logger.error(f"Failed to create database directory: {path_to_check}. Error: {str(e)}")
196
  raise
197
 
198
  def get_default_database_path(self):
 
280
  try:
281
  self.logger.debug(f"Detected change in directory: {path}")
282
 
283
+ # Check if change is just an index file
284
+ changed_files = set(os.listdir(path)) - set(self._last_files.get(path, []))
285
+ if all(f.endswith('.index') for f in changed_files):
286
+ self.logger.debug("Ignoring index file changes")
287
+ # Update last files without triggering refresh
288
+ self._last_files[path] = set(os.listdir(path))
289
+ return
290
+
291
+ # Get current state of CSPR files
292
+ current_cspr_files = set(self._get_cspr_files())
293
+
294
  # Re-validate the path
295
  is_valid, message = self.validate_db_path(self.db_path)
296
 
297
  # Detect specific changes
298
  changes = self._detect_file_changes()
299
 
300
+ # Update last known state
301
+ self._last_cspr_files = current_cspr_files
302
+ self._last_files[path] = set(os.listdir(path))
303
+
304
+ # Always emit validation and state changes
305
  self.db_validation_changed.emit(is_valid, message)
306
+ self.db_state_changed.emit(is_valid, message, changes)
307
 
308
+ # If changes detected, emit files changed signal
309
+ if changes:
310
  self.logger.debug(f"Detected file changes: {changes}")
311
  self.db_files_changed.emit(changes)
 
 
 
312
 
 
 
313
  except Exception as e:
314
  self.logger.error(f"Error handling directory change: {str(e)}")
315
 
 
331
  if f.endswith('.gbff')]
332
 
333
  def check_db_state(self):
334
+ """
335
+ Check database state without clearing invalid paths
336
+ """
337
+ try:
338
+ current_path = self.get_db_path()
339
+ is_valid, message = self.validate_db_path(current_path)
 
 
 
 
 
 
 
 
 
340
 
341
+ # Get list of changes if path is valid
342
+ changes = {} # Initialize as dict instead of list
343
+ if is_valid:
344
+ changes = self._detect_file_changes()
345
+
346
+ # Emit signals but don't modify the path
347
+ self.db_validation_changed.emit(is_valid, message)
348
+ self.db_state_changed.emit(is_valid, message, changes)
349
+
350
+ except Exception as e:
351
+ self.logger.error(f"Error checking database state: {str(e)}")
352
 
353
  def get_organisms_and_endos(self):
354
  """Get mapping of organisms to their endonucleases and files"""
 
613
  except Exception as e:
614
  self.logger.error(f"Error calculating statistics: {str(e)}")
615
  raise
616
+
617
+ def update_db_state(self):
618
+ """Check and update the database state"""
619
+ self.logger.debug("Checking database state")
620
+ if not self.db_path and not self.pending_db_path:
621
+ self.load_database_path()
622
+
623
+ # Use active path for validation
624
+ path_to_check = self.get_active_db_path()
625
+ self.logger.debug(f"Checking state for path: {path_to_check} (pending: {self.pending_db_path}, current: {self.db_path})")
626
+
627
+ is_valid, message = self.validate_db_path(path_to_check)
628
+ self.logger.debug(f"Database validation result - Path: {path_to_check}, Valid: {is_valid}, Message: {message}")
629
+
630
+ # Detect any changes since last check
631
+ changes = self._detect_file_changes()
632
+
633
+ # Check if we should finalize a directory change
634
+ if self.pending_db_path and self.is_changing_directory:
635
+ self.logger.debug("Checking conditions for directory change finalization")
636
+ self.logger.debug(f"Is valid: {is_valid}, Has changes: {bool(changes)}")
637
+
638
+ if is_valid and not changes: # No changes means we're not in the middle of file operations
639
+ self.logger.debug("Attempting to finalize directory change")
640
+ success, finalize_message = self.finalize_directory_change()
641
+ if success:
642
+ self.logger.debug("Directory change finalized successfully")
643
+ # Emit signals
644
+ self.db_validation_changed.emit(True, finalize_message)
645
+ self.db_state_changed.emit(True, finalize_message, changes)
646
+ return
647
+ else:
648
+ self.logger.debug(f"Directory change finalization failed: {finalize_message}")
649
+
650
+ # Emit regular signals
651
+ self.db_validation_changed.emit(is_valid, message)
652
+ if changes:
653
+ self.db_files_changed.emit(changes)
654
+
655
+ self.logger.info(f"Database state checked - Valid: {is_valid}, Changes: {changes}")
656
+
657
+ def cancel_directory_change(self):
658
+ """Cancel the directory change process"""
659
+ self.logger.debug(f"Cancelling directory change process. Previous state - Pending: {self.pending_db_path}, Changing: {self.is_changing_directory}")
660
+ self.pending_db_path = None
661
+ self.is_changing_directory = False
662
+ self.logger.debug("Directory change process cancelled")
663
+
664
+ def finalize_directory_change(self):
665
+ """Finalize the database directory change after successful validation"""
666
+ try:
667
+ if self.pending_db_path and self.is_changing_directory:
668
+ self.logger.debug(f"Finalizing directory change from {self.db_path} to {self.pending_db_path}")
669
+
670
+ # Validate the pending path one final time
671
+ is_valid, message = self.validate_db_path(self.pending_db_path)
672
+ if not is_valid:
673
+ self.logger.warning(f"Cannot finalize directory change: {message}")
674
+ return False, message
675
+
676
+ # Update the current path
677
+ old_path = self.db_path
678
+ self.db_path = self.pending_db_path
679
+
680
+ # Update .env file - try multiple approaches to ensure it works
681
+ try:
682
+ # First attempt: Direct write
683
+ self.config_manager.write_to_env('CSPR_DB', self.db_path)
684
+
685
+ # Second attempt: Use set_env_value
686
+ self.config_manager.set_env_value('CSPR_DB', self.db_path)
687
+
688
+ # Force reload environment variables
689
+ self.config_manager.load_env()
690
+
691
+ # Verify the update
692
+ new_env_value = self.config_manager.get_env_value('CSPR_DB')
693
+ if new_env_value != self.db_path:
694
+ raise Exception(f"Environment variable update failed. Expected: {self.db_path}, Got: {new_env_value}")
695
+
696
+ except Exception as e:
697
+ self.logger.error(f"Error updating environment variable: {str(e)}")
698
+ return False, f"Failed to update environment variable: {str(e)}"
699
+
700
+ # Clear pending state
701
+ self.pending_db_path = None
702
+ self.is_changing_directory = False
703
+
704
+ # Update database state
705
+ self.update_db_state()
706
+
707
+ success_message = f"Successfully changed database directory to:\n{self.db_path}"
708
+ self.logger.info(f"Successfully changed database directory from {old_path} to {self.db_path}")
709
+ self.logger.debug("Directory change finalized successfully")
710
+
711
+ return True, success_message
712
+
713
+ return False, "No pending directory change to finalize"
714
+
715
+ except Exception as e:
716
+ self.logger.error(f"Error finalizing directory change: {str(e)}")
717
+ return False, f"Error finalizing directory change: {str(e)}"
src/models/FindTargetsModel.py CHANGED
@@ -1,21 +1,19 @@
1
- from models.HomeWindowModel import HomeWindowModel
2
  from models.CSPRparser import CSPRparser
 
3
  from models.AnnotationParser import AnnotationParser
4
  import os
5
  from functools import lru_cache
6
  import traceback
7
  from Bio import SeqIO
8
 
9
- class FindTargetsModel(HomeWindowModel):
10
  def __init__(self, global_settings):
11
  super().__init__(global_settings)
12
  self.results = {}
13
- self._parser_cache = {}
14
- self.global_settings.annotation_file_changed.connect(self._on_annotation_file_changed)
15
 
16
- def _on_annotation_file_changed(self, new_annotation_file):
17
- """Clear caches when annotation file changes"""
18
- self.global_settings.logger.debug(f"FindTargetsModel clearing caches for new annotation file: {new_annotation_file}")
19
  self._parser_cache.clear()
20
 
21
  @lru_cache(maxsize=32)
@@ -26,11 +24,9 @@ class FindTargetsModel(HomeWindowModel):
26
  return self._parser_cache[file_path]
27
 
28
  def find_targets(self, input_data):
29
- self.global_settings.logger.debug(f"Received input data: {input_data}")
30
-
31
  organism = input_data['organism']
32
  endo = input_data['endonuclease']
33
- org_files = self.get_organism_to_files()
34
 
35
  self._validate_input(organism, endo, org_files)
36
 
@@ -46,7 +42,7 @@ class FindTargetsModel(HomeWindowModel):
46
  search_func = search_types.get(input_data['search_type'])
47
  if not search_func:
48
  error_msg = f"Invalid search type: {input_data['search_type']}"
49
- self.global_settings.logger.error(error_msg)
50
  raise ValueError(error_msg)
51
 
52
  self.results = search_func(parser, input_data)
@@ -56,12 +52,12 @@ class FindTargetsModel(HomeWindowModel):
56
  def _validate_input(self, organism, endo, org_files):
57
  if organism not in org_files:
58
  error_msg = f"Organism '{organism}' not found in the database. Available organisms: {list(org_files.keys())}"
59
- self.global_settings.logger.error(error_msg)
60
  raise ValueError(error_msg)
61
 
62
  if endo not in org_files[organism]:
63
  error_msg = f"Endonuclease '{endo}' not found for organism '{organism}'. Available endonucleases: {list(org_files[organism].keys())}"
64
- self.global_settings.logger.error(error_msg)
65
  raise ValueError(error_msg)
66
 
67
  def find_targets_by_feature(self, parser, input_data):
@@ -84,18 +80,17 @@ class FindTargetsModel(HomeWindowModel):
84
  self.global_settings.logger.error(f"Annotation file not found: {annotation_file_path}")
85
  raise FileNotFoundError(f"Annotation file not found: {annotation_file_path}")
86
 
87
- self.global_settings.logger.debug(f"Using annotation file: {annotation_file_path}")
88
-
89
  # Split search queries by newlines and remove empty lines
90
  search_queries = [q.strip() for q in input_data['search_query'].split('\n') if q.strip()]
91
 
92
- annotation_parser = AnnotationParser(self.global_settings)
93
- annotation_parser.set_annotation_file(annotation_file_path)
 
94
 
95
  # Process each query and combine results
96
  all_results = []
97
  for search_query in search_queries:
98
- results_list = annotation_parser.genbank_search([search_query])
99
 
100
  for record_id, feature_info in results_list:
101
  location = feature_info['feature_location']
@@ -355,3 +350,11 @@ class FindTargetsModel(HomeWindowModel):
355
  'endonuclease': target[5]
356
  })
357
  return formatted_results
 
 
 
 
 
 
 
 
 
 
1
  from models.CSPRparser import CSPRparser
2
+ from models.BaseModel import BaseModel
3
  from models.AnnotationParser import AnnotationParser
4
  import os
5
  from functools import lru_cache
6
  import traceback
7
  from Bio import SeqIO
8
 
9
+ class FindTargetsModel(BaseModel):
10
  def __init__(self, global_settings):
11
  super().__init__(global_settings)
12
  self.results = {}
13
+ self._parser_cache = {}
 
14
 
15
+ def _clear_caches(self):
16
+ """Clear all model-specific caches"""
 
17
  self._parser_cache.clear()
18
 
19
  @lru_cache(maxsize=32)
 
24
  return self._parser_cache[file_path]
25
 
26
  def find_targets(self, input_data):
 
 
27
  organism = input_data['organism']
28
  endo = input_data['endonuclease']
29
+ org_files = self.global_settings.get_organism_files()
30
 
31
  self._validate_input(organism, endo, org_files)
32
 
 
42
  search_func = search_types.get(input_data['search_type'])
43
  if not search_func:
44
  error_msg = f"Invalid search type: {input_data['search_type']}"
45
+ self.logger.error(error_msg)
46
  raise ValueError(error_msg)
47
 
48
  self.results = search_func(parser, input_data)
 
52
  def _validate_input(self, organism, endo, org_files):
53
  if organism not in org_files:
54
  error_msg = f"Organism '{organism}' not found in the database. Available organisms: {list(org_files.keys())}"
55
+ self.logger.error(error_msg)
56
  raise ValueError(error_msg)
57
 
58
  if endo not in org_files[organism]:
59
  error_msg = f"Endonuclease '{endo}' not found for organism '{organism}'. Available endonucleases: {list(org_files[organism].keys())}"
60
+ self.logger.error(error_msg)
61
  raise ValueError(error_msg)
62
 
63
  def find_targets_by_feature(self, parser, input_data):
 
80
  self.global_settings.logger.error(f"Annotation file not found: {annotation_file_path}")
81
  raise FileNotFoundError(f"Annotation file not found: {annotation_file_path}")
82
 
 
 
83
  # Split search queries by newlines and remove empty lines
84
  search_queries = [q.strip() for q in input_data['search_query'].split('\n') if q.strip()]
85
 
86
+ # Use existing annotation parser from BaseModel
87
+ if self.annotation_parser.annotation_file_name != annotation_file_path:
88
+ self.annotation_parser.set_annotation_file(annotation_file_path)
89
 
90
  # Process each query and combine results
91
  all_results = []
92
  for search_query in search_queries:
93
+ results_list = self.annotation_parser.genbank_search([search_query])
94
 
95
  for record_id, feature_info in results_list:
96
  location = feature_info['feature_location']
 
350
  'endonuclease': target[5]
351
  })
352
  return formatted_results
353
+
354
+ def get_cspr_file_path(self, input_data):
355
+ """Get the path to the CSPR file for the given input data"""
356
+ org_files = self.global_settings.get_organism_files()
357
+ return os.path.join(
358
+ self.global_settings.get_db_path(),
359
+ org_files[input_data['organism']][input_data['endonuclease']][0]
360
+ )
src/models/GenerateLibraryModel.py CHANGED
@@ -1,21 +1,30 @@
1
  from models.CSPRparser import CSPRparser
2
- from models.HomeWindowModel import HomeWindowModel
3
  import os
4
  import re
5
  import traceback
 
6
 
7
- class GenerateLibraryModel(HomeWindowModel):
 
 
8
  def __init__(self, global_settings):
9
- super().__init__(global_settings)
 
10
  self.logger = global_settings.logger
11
  self.parser = None
12
  self.targets_data = {}
13
  self._deleted_targets = {}
 
14
 
15
  def initialize_parser(self, cspr_file):
16
  """Initialize CSPR parser"""
17
  self.parser = CSPRparser(cspr_file, self.global_settings.get_casper_info_path())
18
 
 
 
 
 
19
  def generate_library(self, selected_targets, settings):
20
  """Generate library with given settings"""
21
  try:
@@ -29,26 +38,114 @@ class GenerateLibraryModel(HomeWindowModel):
29
  settings['target_range_start'],
30
  settings['target_range_end']
31
  )
32
-
33
- # Generate output for each target
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  output_data = self._generate_output(
35
  processed_targets,
36
  settings['guides_per_gene'],
37
  settings['space_between_guides']
38
  )
39
-
40
- self.logger.debug(f"Output data: {output_data}")
41
 
42
  # Write output to file
43
  self._write_output(output_data, settings)
44
 
45
- return True
46
-
 
 
 
47
  except Exception as e:
48
- self.logger.error(f"Error generating library: {str(e)}")
49
- self.logger.error(traceback.format_exc())
50
  raise
51
-
52
  def _process_targets(self, targets, min_score, five_prime_seq, start_range, end_range):
53
  """Process and filter targets based on criteria"""
54
  processed = {}
@@ -73,17 +170,11 @@ class GenerateLibraryModel(HomeWindowModel):
73
  else:
74
  self._deleted_targets[gene_name].append(target_data)
75
 
76
- # Sort targets for each gene
77
  for gene in processed:
78
- # First sort by score (ascending)
79
- processed[gene].sort(key=lambda x: float(x['score']))
80
-
81
- # Then sort by position (ascending)
82
- processed[gene].sort(key=lambda x: abs(int(x['position'])))
83
-
84
- # Reverse list if gene is on negative strand
85
- if processed[gene] and processed[gene][0].get('strand', '+') == '-':
86
- processed[gene].reverse()
87
 
88
  return processed
89
 
@@ -93,14 +184,18 @@ class GenerateLibraryModel(HomeWindowModel):
93
  # Score filter - convert score to float and compare
94
  target_score = float(target.get('score', 0))
95
  if target_score < min_score:
96
- self.logger.debug(f"Target failed score filter: {target_score} < {min_score}")
97
  return False
98
 
99
- # Poly-T filter
 
 
100
  if re.search("T{5,10}", target['sequence']):
101
  self.logger.debug(f"Target failed poly-T filter: {target['sequence']}")
102
  return False
103
 
 
 
104
  # 5' sequence filter
105
  if five_prime_seq and not target['sequence'].startswith(five_prime_seq.upper()):
106
  self.logger.debug(f"Target failed 5' sequence filter")
@@ -146,47 +241,77 @@ class GenerateLibraryModel(HomeWindowModel):
146
  return 0
147
 
148
  def _generate_output(self, processed_targets, guides_per_gene, space_between):
 
149
  output = {}
150
 
151
  for gene_id, targets in processed_targets.items():
152
  output[gene_id] = []
153
- i = 0
154
- vec_index = 0
 
 
 
 
 
 
 
 
 
155
  prev_target = None
156
 
157
  while i < guides_per_gene:
158
  if len(targets) == 0 or vec_index >= len(targets):
159
  break
160
-
161
  current = targets[vec_index]
162
 
163
- # Check spacing from previous target
164
- if prev_target is None or abs(int(current['position']) - int(prev_target['position'])) >= space_between:
165
- # If current target has better score than previous
166
- if (prev_target and float(current['score']) > float(prev_target['score'])):
167
- output[gene_id].pop()
168
- output[gene_id].append(current)
169
- else:
170
- output[gene_id].append(current)
171
  prev_target = current
172
  i += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
  vec_index += 1
175
- if vec_index >= len(targets):
176
- break
177
-
178
- # Add deleted targets if needed
179
- if len(output[gene_id]) < guides_per_gene:
180
- deleted_sorted = sorted(
181
- self._deleted_targets.get(gene_id, []),
182
- key=lambda x: (float(x['score']), abs(int(x['position'])))
183
- )
184
 
185
- for deleted_target in deleted_sorted:
186
- if len(output[gene_id]) >= guides_per_gene:
187
- break
188
- deleted_target['modified'] = True
189
- output[gene_id].append(deleted_target)
190
 
191
  return output
192
 
 
1
  from models.CSPRparser import CSPRparser
2
+ from models.OffTargetModel import OffTargetModel
3
  import os
4
  import re
5
  import traceback
6
+ from PyQt6.QtCore import QObject, pyqtSignal
7
 
8
+ class GenerateLibraryModel(QObject):
9
+ progress_updated = pyqtSignal(int) # Signal to emit progress updates
10
+
11
  def __init__(self, global_settings):
12
+ super().__init__()
13
+ self.global_settings = global_settings
14
  self.logger = global_settings.logger
15
  self.parser = None
16
  self.targets_data = {}
17
  self._deleted_targets = {}
18
+ self.off_target_model = OffTargetModel(global_settings)
19
 
20
  def initialize_parser(self, cspr_file):
21
  """Initialize CSPR parser"""
22
  self.parser = CSPRparser(cspr_file, self.global_settings.get_casper_info_path())
23
 
24
+ def get_organism_to_files(self):
25
+ """Get mapping of organisms to their files from global settings"""
26
+ return self.global_settings.get_organism_files()
27
+
28
  def generate_library(self, selected_targets, settings):
29
  """Generate library with given settings"""
30
  try:
 
38
  settings['target_range_start'],
39
  settings['target_range_end']
40
  )
41
+
42
+ if settings.get('find_off_targets'):
43
+ # Write targets to temp file for off-target analysis
44
+ self._write_targets_to_temp(processed_targets)
45
+
46
+ # Get organism and endonuclease from home window
47
+ if hasattr(self.global_settings, '_current_home_window'):
48
+ organism = self.global_settings._current_home_window.view.combo_box_organism.currentText()
49
+ endonuclease = self.global_settings._current_home_window.view.combo_box_endonuclease.currentText()
50
+ else:
51
+ raise ValueError("Could not access home window to get organism and endonuclease")
52
+
53
+ if not organism or not endonuclease:
54
+ raise ValueError("Could not determine organism or endonuclease from home window")
55
+
56
+ self.logger.debug(f"Using organism: {organism} and endonuclease: {endonuclease} for off-target analysis")
57
+
58
+ # Setup off-target parameters
59
+ off_target_params = {
60
+ 'organism': organism,
61
+ 'endonuclease': endonuclease,
62
+ 'max_mismatches': 4, # Default value from old implementation
63
+ 'tolerance': 0.05, # Default value from old implementation
64
+ 'average_output': True,
65
+ 'save_output': False,
66
+ 'output_filename': '',
67
+ 'targets': selected_targets,
68
+ 'annotation_file': self.global_settings.get_current_annotation_file()
69
+ }
70
+
71
+ # Connect to off-target model signals
72
+ self.off_target_model.progress_updated.connect(self._handle_off_target_progress)
73
+ self.off_target_model.results_ready.connect(lambda results: self._handle_off_target_results(results, processed_targets, settings))
74
+
75
+ # Start off-target analysis
76
+ self.off_target_model.start_analysis(off_target_params)
77
+ return True
78
+ else:
79
+ # Generate output for each target
80
+ output_data = self._generate_output(
81
+ processed_targets,
82
+ settings['guides_per_gene'],
83
+ settings['space_between_guides']
84
+ )
85
+
86
+ self.logger.debug(f"Output data: {output_data}")
87
+
88
+ # Write output to file
89
+ self._write_output(output_data, settings)
90
+ return True
91
+
92
+ except Exception as e:
93
+ self.logger.error(f"Error generating library: {str(e)}")
94
+ self.logger.error(traceback.format_exc())
95
+ raise
96
+
97
+ def _write_targets_to_temp(self, processed_targets):
98
+ """Write targets to temp file for off-target analysis"""
99
+ try:
100
+ temp_path = os.path.join(self.global_settings.get_db_path(), 'temp.txt')
101
+
102
+ with open(temp_path, 'w') as f:
103
+ for gene in processed_targets:
104
+ for target in processed_targets[gene]:
105
+ # Format: position;sequence;pam;score;strand
106
+ entry = f"{target['position']};{target['sequence']};{target['pam']};{target['score']};{target['strand']}\n"
107
+ f.write(entry)
108
+
109
+ self.logger.debug(f"Wrote targets to temp file: {temp_path}")
110
+
111
+ except Exception as e:
112
+ self.logger.error(f"Error writing targets to temp file: {str(e)}")
113
+ raise
114
+
115
+ def _handle_off_target_progress(self, value, status):
116
+ """Handle progress updates from off-target analysis"""
117
+ self.progress_updated.emit(value)
118
+
119
+ def _handle_off_target_results(self, results, processed_targets, settings):
120
+ """Handle results from off-target analysis"""
121
+ try:
122
+ scores_dict, _ = results
123
+
124
+ # Update targets with off-target scores
125
+ for gene in processed_targets:
126
+ for target in processed_targets[gene]:
127
+ if target['sequence'] in scores_dict:
128
+ target['off_target_score'] = scores_dict[target['sequence']]
129
+
130
+ # Generate output with updated targets
131
  output_data = self._generate_output(
132
  processed_targets,
133
  settings['guides_per_gene'],
134
  settings['space_between_guides']
135
  )
 
 
136
 
137
  # Write output to file
138
  self._write_output(output_data, settings)
139
 
140
+ # Clean up temp file
141
+ temp_path = os.path.join(self.global_settings.get_db_path(), 'temp.txt')
142
+ if os.path.exists(temp_path):
143
+ os.remove(temp_path)
144
+
145
  except Exception as e:
146
+ self.logger.error(f"Error handling off-target results: {str(e)}")
 
147
  raise
148
+
149
  def _process_targets(self, targets, min_score, five_prime_seq, start_range, end_range):
150
  """Process and filter targets based on criteria"""
151
  processed = {}
 
170
  else:
171
  self._deleted_targets[gene_name].append(target_data)
172
 
173
+ # Log first 5 targets for each gene for debugging
174
  for gene in processed:
175
+ if processed[gene]:
176
+ self.logger.debug(f"First 5 targets for gene {gene}: {[t['score'] for t in processed[gene][:5]]}")
177
+ self.logger.debug(f"First 5 target positions for gene {gene}: {[t['position'] for t in processed[gene][:5]]}")
 
 
 
 
 
 
178
 
179
  return processed
180
 
 
184
  # Score filter - convert score to float and compare
185
  target_score = float(target.get('score', 0))
186
  if target_score < min_score:
187
+ self.logger.debug(f"Target failed score filter: {target_score} < {min_score}, target: {target}")
188
  return False
189
 
190
+ self.logger.debug(f"Target passed score filter: {target_score} >= {min_score}, target: {target}")
191
+
192
+ # Poly-T filter (5-10 consecutive T's)
193
  if re.search("T{5,10}", target['sequence']):
194
  self.logger.debug(f"Target failed poly-T filter: {target['sequence']}")
195
  return False
196
 
197
+ self.logger.debug(f"Target passed poly-T filter: {target['sequence']}")
198
+
199
  # 5' sequence filter
200
  if five_prime_seq and not target['sequence'].startswith(five_prime_seq.upper()):
201
  self.logger.debug(f"Target failed 5' sequence filter")
 
241
  return 0
242
 
243
  def _generate_output(self, processed_targets, guides_per_gene, space_between):
244
+ """Generate output with proper spacing between guides"""
245
  output = {}
246
 
247
  for gene_id, targets in processed_targets.items():
248
  output[gene_id] = []
249
+
250
+ # First sort by score (descending)
251
+ targets.sort(key=lambda x: float(x['score']), reverse=True)
252
+ self.logger.debug(f"First 5 targets positions for gene {gene_id} by score: {[(t['position'], t['score']) for t in targets[:5]]}")
253
+
254
+ # Then sort by position
255
+ targets.sort(key=lambda x: abs(int(x['position'])))
256
+ self.logger.debug(f"First 5 targets positions for gene {gene_id}: {[t['position'] for t in targets[:5]]}")
257
+
258
+ i = 0 # Counter for selected guides
259
+ vec_index = 0 # Index for current target being considered
260
  prev_target = None
261
 
262
  while i < guides_per_gene:
263
  if len(targets) == 0 or vec_index >= len(targets):
264
  break
265
+
266
  current = targets[vec_index]
267
 
268
+ # For first target, just add it
269
+ if prev_target is None:
270
+ output[gene_id].append(current)
 
 
 
 
 
271
  prev_target = current
272
  i += 1
273
+ else:
274
+ # Check spacing from previous target
275
+ distance = abs(int(current['position']) - int(prev_target['position']))
276
+
277
+ if distance >= space_between:
278
+ # Look ahead for better scoring targets within this space
279
+ best_target = current
280
+ look_ahead_index = vec_index + 1
281
+
282
+ while look_ahead_index < len(targets):
283
+ next_target = targets[look_ahead_index]
284
+ next_distance = abs(int(next_target['position']) - int(prev_target['position']))
285
+
286
+ # If we've gone too far, break
287
+ if next_distance >= space_between:
288
+ break
289
+
290
+ # If this target has better score
291
+ if float(next_target['score']) > float(best_target['score']):
292
+ best_target = next_target
293
+
294
+ look_ahead_index += 1
295
+
296
+ output[gene_id].append(best_target)
297
+ prev_target = best_target
298
+ i += 1
299
+
300
+ # Move vec_index past the selected target's position
301
+ while vec_index < len(targets) and abs(int(targets[vec_index]['position'])) <= abs(int(best_target['position'])):
302
+ vec_index += 1
303
+ continue
304
 
305
  vec_index += 1
306
+
307
+ # Sort final output by position
308
+ output[gene_id].sort(key=lambda x: abs(int(x['position'])))
309
+
310
+ # If gene is on negative strand, reverse the order
311
+ if output[gene_id] and output[gene_id][0].get('strand', '+') == '-':
312
+ output[gene_id].reverse()
 
 
313
 
314
+ self.logger.debug(f"Selected targets positions for gene {gene_id}: {[t['position'] for t in output[gene_id]]}")
 
 
 
 
315
 
316
  return output
317
 
src/models/GlobalSettings.py CHANGED
@@ -68,21 +68,46 @@ class GlobalSettings(QObject):
68
  self._preloading_modules = {}
69
  self.main_window = None
70
 
71
- # Only preload essential controllers for startup
72
- self._preload_essential_controllers()
73
-
74
- # Start background loading of commonly used modules
75
- self._background_load_common_modules()
76
-
77
  self.config_manager = ConfigManager(app_dir_path=self.app_dir_path, logger=self.logger)
78
  self.config_manager.load_env()
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  self.is_first_time_startup = self.config_manager.get_env_value('FIRST_TIME_START', 'TRUE').upper() == 'TRUE'
81
 
82
- # Defer database initialization until needed
83
- self._initialize_directories()
84
  self._init_db_manager()
85
 
 
 
 
 
 
 
86
  # Defer theme initialization
87
  self._init_theme_settings()
88
 
@@ -170,6 +195,8 @@ class GlobalSettings(QObject):
170
 
171
  def validate_db_path(self, path):
172
  """Validate the given database path"""
 
 
173
  return self.db_manager.validate_db_path(path)
174
 
175
  def save_db_path(self, path):
@@ -295,8 +322,6 @@ class GlobalSettings(QObject):
295
  def _get_window_class(self, window_name):
296
  """Get the controller class with optimized loading"""
297
  try:
298
- start_time = time.time()
299
-
300
  # Check if module is already cached
301
  module_path = f"controllers.{window_name}Controller"
302
  if module_path in self._module_cache:
@@ -329,7 +354,6 @@ class GlobalSettings(QObject):
329
  if not hasattr(controller_module, class_name):
330
  raise AttributeError(f"Controller module does not contain class {class_name}")
331
 
332
- self.logger.debug(f"Window class retrieval took: {time.time() - start_time:.2f} seconds")
333
  return getattr(controller_module, class_name)
334
 
335
  except Exception as e:
@@ -350,11 +374,15 @@ class GlobalSettings(QObject):
350
  self.logger.error(f"Error creating window {window_name}: {str(e)}")
351
  raise
352
 
353
- def get_startup_window(self):
354
- if not hasattr(self, '_startup_window'):
355
- from controllers.StartupWindowController import StartupWindowController
356
- self._startup_window = StartupWindowController(self)
357
- return self._startup_window
 
 
 
 
358
 
359
  def get_home_window(self):
360
  """Get or create home window with proper initialization"""
@@ -391,7 +419,13 @@ class GlobalSettings(QObject):
391
  def _background_load_common_modules(self):
392
  """Start background loading of commonly used modules"""
393
  try:
394
- common_modules = ["MultitargetingWindow", "PopulationAnalysisWindow"]
 
 
 
 
 
 
395
  for module_name in common_modules:
396
  if (module_name not in self._module_cache and
397
  module_name not in self._preloading_modules):
@@ -411,7 +445,6 @@ class GlobalSettings(QObject):
411
  preloader = self._preloading_modules[module_name]
412
  if not preloader.isRunning(): # Only remove if thread is finished
413
  del self._preloading_modules[module_name]
414
- self.logger.debug(f"Module {module_name} preloaded successfully")
415
  except Exception as e:
416
  self.logger.error(f"Error handling preloaded module: {str(e)}")
417
 
@@ -501,29 +534,15 @@ class GlobalSettings(QObject):
501
  def set_current_annotation_file(self, annotation_file):
502
  """Set the current annotation file and notify listeners"""
503
  try:
504
- if not hasattr(self, '_current_annotation_file'):
505
- self._current_annotation_file = None
506
-
507
  if self._current_annotation_file != annotation_file:
508
  self._current_annotation_file = annotation_file
509
- self.logger.debug(f"Current annotation file changed to: {annotation_file}")
510
  self.annotation_file_changed.emit(annotation_file)
511
  except Exception as e:
512
  self.logger.error(f"Error setting current annotation file: {str(e)}")
513
 
514
  def get_current_annotation_file(self):
515
  """Get the currently selected annotation file"""
516
- try:
517
- if not self._current_annotation_file and hasattr(self, '_current_home_window'):
518
- # Try to get from home window if not set
519
- home_controller = self._current_home_window
520
- if hasattr(home_controller, 'view'):
521
- self._current_annotation_file = home_controller.view.get_annotation_file()
522
- self.logger.debug(f"Got annotation file from home window: {self._current_annotation_file}")
523
- return self._current_annotation_file
524
- except Exception as e:
525
- self.logger.error(f"Error getting current annotation file: {str(e)}")
526
- return None
527
 
528
  def get_scoring_options_window(self, view_targets_controller):
529
  """Create and return ScoringOptionsController instance"""
@@ -575,5 +594,125 @@ class GlobalSettings(QObject):
575
  self.logger.error(f"Error adjusting path: {str(e)}")
576
  return path # Return original path if adjustment fails
577
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
  # Global instance
579
  global_settings = None
 
68
  self._preloading_modules = {}
69
  self.main_window = None
70
 
71
+ # Initialize config manager first
 
 
 
 
 
72
  self.config_manager = ConfigManager(app_dir_path=self.app_dir_path, logger=self.logger)
73
  self.config_manager.load_env()
74
 
75
+ # Initialize directories
76
+ self._initialize_directories()
77
+
78
+ # Check if there are CSPR files in the current database path
79
+ current_db_path = self.config_manager.get_env_value('CSPR_DB', '')
80
+ if current_db_path:
81
+ try:
82
+ import glob, os
83
+ cspr_files = glob.glob(os.path.join(current_db_path, "*.cspr"))
84
+ if not cspr_files:
85
+ # No CSPR files found, but keep the path
86
+ self.logger.info(f"No CSPR files found in {current_db_path}, but keeping the path")
87
+ # Only set first time startup to TRUE if there was no previous path
88
+ if not self.config_manager.get_env_value('CSPR_DB', ''):
89
+ self.config_manager.set_env_value('FIRST_TIME_START', 'TRUE')
90
+ else:
91
+ current_value = self.config_manager.get_env_value('FIRST_TIME_START', 'TRUE')
92
+ if current_value.upper() != 'FALSE':
93
+ self.config_manager.set_env_value('FIRST_TIME_START', 'FALSE')
94
+ except Exception as e:
95
+ self.logger.error(f"Error checking CSPR files: {str(e)}")
96
+ # Keep the path even on error
97
+ self.logger.info(f"Keeping database path despite error: {current_db_path}")
98
+
99
+ # Set first time startup flag
100
  self.is_first_time_startup = self.config_manager.get_env_value('FIRST_TIME_START', 'TRUE').upper() == 'TRUE'
101
 
102
+ # Initialize database manager after potential path reset
 
103
  self._init_db_manager()
104
 
105
+ # Only preload essential controllers after determining startup state
106
+ self._preload_essential_controllers()
107
+
108
+ # Start background loading of commonly used modules
109
+ self._background_load_common_modules()
110
+
111
  # Defer theme initialization
112
  self._init_theme_settings()
113
 
 
195
 
196
  def validate_db_path(self, path):
197
  """Validate the given database path"""
198
+ print("path", path)
199
+ print("db.manager validate_db_path", self.db_manager.validate_db_path(path))
200
  return self.db_manager.validate_db_path(path)
201
 
202
  def save_db_path(self, path):
 
322
  def _get_window_class(self, window_name):
323
  """Get the controller class with optimized loading"""
324
  try:
 
 
325
  # Check if module is already cached
326
  module_path = f"controllers.{window_name}Controller"
327
  if module_path in self._module_cache:
 
354
  if not hasattr(controller_module, class_name):
355
  raise AttributeError(f"Controller module does not contain class {class_name}")
356
 
 
357
  return getattr(controller_module, class_name)
358
 
359
  except Exception as e:
 
374
  self.logger.error(f"Error creating window {window_name}: {str(e)}")
375
  raise
376
 
377
+ def get_startup_window(self, keep_db_path=False):
378
+ """
379
+ Creates and returns a new startup window controller
380
+
381
+ Args:
382
+ keep_db_path (bool): If True, keeps the existing DB path when initializing startup
383
+ """
384
+ from controllers.StartupWindowController import StartupWindowController
385
+ return StartupWindowController(self, keep_db_path=keep_db_path)
386
 
387
  def get_home_window(self):
388
  """Get or create home window with proper initialization"""
 
419
  def _background_load_common_modules(self):
420
  """Start background loading of commonly used modules"""
421
  try:
422
+ common_modules = [
423
+ "MultitargetingWindow",
424
+ "PopulationAnalysisWindow",
425
+ "NewGenomeWindow",
426
+ "NewEndonuclease",
427
+ "NCBIWindow"
428
+ ]
429
  for module_name in common_modules:
430
  if (module_name not in self._module_cache and
431
  module_name not in self._preloading_modules):
 
445
  preloader = self._preloading_modules[module_name]
446
  if not preloader.isRunning(): # Only remove if thread is finished
447
  del self._preloading_modules[module_name]
 
448
  except Exception as e:
449
  self.logger.error(f"Error handling preloaded module: {str(e)}")
450
 
 
534
  def set_current_annotation_file(self, annotation_file):
535
  """Set the current annotation file and notify listeners"""
536
  try:
 
 
 
537
  if self._current_annotation_file != annotation_file:
538
  self._current_annotation_file = annotation_file
 
539
  self.annotation_file_changed.emit(annotation_file)
540
  except Exception as e:
541
  self.logger.error(f"Error setting current annotation file: {str(e)}")
542
 
543
  def get_current_annotation_file(self):
544
  """Get the currently selected annotation file"""
545
+ return self._current_annotation_file
 
 
 
 
 
 
 
 
 
 
546
 
547
  def get_scoring_options_window(self, view_targets_controller):
548
  """Create and return ScoringOptionsController instance"""
 
594
  self.logger.error(f"Error adjusting path: {str(e)}")
595
  return path # Return original path if adjustment fails
596
 
597
+ def get_stylesheet(self):
598
+ """Get the current theme's stylesheet"""
599
+ current_theme = self.get_theme()
600
+ return self.get_dark_stylesheet() if current_theme == "dark" else self.get_light_stylesheet()
601
+
602
+ def get_dark_stylesheet(self):
603
+ """Get dark theme stylesheet"""
604
+ theme = {
605
+ "bg_color": "#2b2b2b",
606
+ "fg_color": "#ffffff",
607
+ "button_bg_color": "#3a3a3a",
608
+ "button_border_color": "#5a5a5a",
609
+ "button_hover_bg_color": "#4a4a4a",
610
+ "input_bg_color": "#3a3a3a",
611
+ "input_border_color": "#5a5a5a",
612
+ "progress_bar_bg": "#3a3a3a",
613
+ "progress_bar_chunk": "#51b85e"
614
+ }
615
+ return self._get_themed_stylesheet(theme)
616
+
617
+ def get_light_stylesheet(self):
618
+ """Get light theme stylesheet"""
619
+ theme = {
620
+ "bg_color": "#f0f0f0",
621
+ "fg_color": "#000000",
622
+ "button_bg_color": "#e0e0e0",
623
+ "button_border_color": "#c0c0c0",
624
+ "button_hover_bg_color": "#d0d0d0",
625
+ "input_bg_color": "#ffffff",
626
+ "input_border_color": "#c0c0c0",
627
+ "progress_bar_bg": "#e0e0e0",
628
+ "progress_bar_chunk": "#51b85e"
629
+ }
630
+ return self._get_themed_stylesheet(theme)
631
+
632
+ def _get_themed_stylesheet(self, theme):
633
+ """Generate stylesheet based on theme colors"""
634
+ return f"""
635
+ QMainWindow, QWidget {{
636
+ background-color: {theme['bg_color']};
637
+ color: {theme['fg_color']};
638
+ }}
639
+ QPushButton {{
640
+ background-color: {theme['button_bg_color']};
641
+ border: 1px solid {theme['button_border_color']};
642
+ padding: 5px;
643
+ min-width: 80px;
644
+ }}
645
+ QPushButton:hover {{
646
+ background-color: {theme['button_hover_bg_color']};
647
+ }}
648
+ QLineEdit {{
649
+ background-color: {theme['input_bg_color']};
650
+ border: 1px solid {theme['input_border_color']};
651
+ padding: 5px;
652
+ }}
653
+ QComboBox {{
654
+ background-color: {theme['input_bg_color']};
655
+ border: 1px solid {theme['input_border_color']};
656
+ padding: 5px;
657
+ }}
658
+ QComboBox:hover {{
659
+ background-color: {theme['button_hover_bg_color']};
660
+ }}
661
+ QRadioButton {{
662
+ color: {theme['fg_color']};
663
+ }}
664
+ QProgressBar {{
665
+ border: 1px solid {theme['button_border_color']};
666
+ background-color: {theme['progress_bar_bg']};
667
+ text-align: center;
668
+ }}
669
+ QProgressBar::chunk {{
670
+ background-color: {theme['progress_bar_chunk']};
671
+ }}
672
+ QGroupBox {{
673
+ border: 1px solid {theme['button_border_color']};
674
+ margin-top: 0.5em;
675
+ padding-top: 0.5em;
676
+ }}
677
+ QGroupBox::title {{
678
+ color: {theme['fg_color']};
679
+ subcontrol-origin: margin;
680
+ left: 10px;
681
+ padding: 0 3px 0 3px;
682
+ }}
683
+ QDoubleSpinBox {{
684
+ background-color: {theme['input_bg_color']};
685
+ border: 1px solid {theme['input_border_color']};
686
+ padding: 5px;
687
+ }}
688
+ """
689
+
690
+ def get_organism_files(self):
691
+ """Get mapping of organisms to their files from database manager"""
692
+ organism_files, _ = self.db_manager.get_organisms_and_endos()
693
+ return organism_files
694
+
695
+ def get_groupbox_style(self) -> str:
696
+ """Get the standardized groupbox style with green accent color"""
697
+ return """
698
+ QGroupBox:title {
699
+ subcontrol-origin: margin;
700
+ left: 10px;
701
+ padding: 0 5px 0 5px;
702
+ }
703
+ QGroupBox {
704
+ border: 2px solid rgb(111,181,110);
705
+ border-radius: 9px;
706
+ margin-top: 10px;
707
+ font: bold 14pt 'Arial';
708
+ }
709
+ QGroupBox#grpNavigationMenu {
710
+ border: 2px dashed rgb(88,89,91);
711
+ border-radius: 9px;
712
+ margin-top: 10px;
713
+ font: bold 14pt 'Arial';
714
+ }
715
+ """
716
+
717
  # Global instance
718
  global_settings = None
src/models/HomeWindowModel.py CHANGED
@@ -5,15 +5,16 @@ from utils.ui import show_error
5
  from models.DatabaseManager import FileChangeType
6
 
7
  class HomeWindowModel:
8
- def __init__(self, global_settings):
9
  self.global_settings = global_settings
10
  self.logger = global_settings.get_logger()
11
- self.data = {
12
- 'organism_to_files': {},
13
- 'organism_to_endonuclease': {},
14
- 'annotation_files': set() # Using set for efficient updates
15
- }
16
- self.load_data()
 
17
 
18
  def load_data(self) -> None:
19
  """Load all required data"""
@@ -56,8 +57,8 @@ class HomeWindowModel:
56
  """Load organism and endonuclease data from CSPR files"""
57
  try:
58
  # Clear existing data
59
- self.data["organism_to_files"] = {}
60
- self.data["organism_to_endonuclease"] = {}
61
 
62
  cspr_files = glob.glob(os.path.join(self.global_settings.get_db_path(), "*.cspr"))
63
 
@@ -70,20 +71,20 @@ class HomeWindowModel:
70
  organism = f.readline().strip().replace("GENOME: ", '')
71
 
72
  # Update organism to files mapping
73
- if organism not in self.data["organism_to_files"]:
74
- self.data["organism_to_files"][organism] = {}
75
- self.data["organism_to_files"][organism][endonuclease] = [
76
  file_name,
77
  file_name.replace(".cspr", "_repeats.db")
78
  ]
79
 
80
  # Update organism to endonuclease mapping
81
- if organism not in self.data["organism_to_endonuclease"]:
82
- self.data["organism_to_endonuclease"][organism] = []
83
- if endonuclease not in self.data["organism_to_endonuclease"][organism]:
84
- self.data["organism_to_endonuclease"][organism].append(endonuclease)
85
 
86
- self.logger.debug(f"Loaded data for {len(self.data['organism_to_files'])} organisms")
87
 
88
  except Exception as e:
89
  self.logger.error(f"Error loading organisms and endonucleases: {str(e)}")
@@ -99,12 +100,12 @@ class HomeWindowModel:
99
  )
100
 
101
  # Process files
102
- self.data["annotation_files"] = {
103
  os.path.basename(file) for file in annotation_files
104
  if not file.endswith('.index') # Exclude index files
105
  }
106
 
107
- self.logger.debug(f"Loaded {len(self.data['annotation_files'])} annotation files")
108
 
109
  except Exception as e:
110
  self.logger.error(f"Error loading annotation files: {str(e)}")
@@ -112,15 +113,15 @@ class HomeWindowModel:
112
 
113
  def get_organism_to_files(self) -> Dict[str, Dict[str, List[str]]]:
114
  """Get mapping of organisms to their files"""
115
- return self.data['organism_to_files']
116
 
117
  def get_organism_to_endonuclease(self) -> Dict[str, List[str]]:
118
  """Get mapping of organisms to their endonucleases"""
119
- return self.data.get("organism_to_endonuclease", {})
120
 
121
  def get_annotation_files(self) -> List[str]:
122
  """Get list of annotation files"""
123
- return sorted(self.data.get("annotation_files", set()), key=str.lower)
124
 
125
  def find_targets(self, input_data: dict) -> None:
126
  pass
 
5
  from models.DatabaseManager import FileChangeType
6
 
7
  class HomeWindowModel:
8
+ def __init__(self, global_settings, skip_initial_load=False):
9
  self.global_settings = global_settings
10
  self.logger = global_settings.get_logger()
11
+ self._organism_to_files = {}
12
+ self._organism_to_endonuclease = {}
13
+ self._annotation_files = []
14
+
15
+ # Only load data if not skipped
16
+ if not skip_initial_load:
17
+ self.load_data()
18
 
19
  def load_data(self) -> None:
20
  """Load all required data"""
 
57
  """Load organism and endonuclease data from CSPR files"""
58
  try:
59
  # Clear existing data
60
+ self._organism_to_files = {}
61
+ self._organism_to_endonuclease = {}
62
 
63
  cspr_files = glob.glob(os.path.join(self.global_settings.get_db_path(), "*.cspr"))
64
 
 
71
  organism = f.readline().strip().replace("GENOME: ", '')
72
 
73
  # Update organism to files mapping
74
+ if organism not in self._organism_to_files:
75
+ self._organism_to_files[organism] = {}
76
+ self._organism_to_files[organism][endonuclease] = [
77
  file_name,
78
  file_name.replace(".cspr", "_repeats.db")
79
  ]
80
 
81
  # Update organism to endonuclease mapping
82
+ if organism not in self._organism_to_endonuclease:
83
+ self._organism_to_endonuclease[organism] = []
84
+ if endonuclease not in self._organism_to_endonuclease[organism]:
85
+ self._organism_to_endonuclease[organism].append(endonuclease)
86
 
87
+ self.logger.debug(f"Loaded data for {len(self._organism_to_files)} organisms")
88
 
89
  except Exception as e:
90
  self.logger.error(f"Error loading organisms and endonucleases: {str(e)}")
 
100
  )
101
 
102
  # Process files
103
+ self._annotation_files = {
104
  os.path.basename(file) for file in annotation_files
105
  if not file.endswith('.index') # Exclude index files
106
  }
107
 
108
+ self.logger.debug(f"Loaded {len(self._annotation_files)} annotation files")
109
 
110
  except Exception as e:
111
  self.logger.error(f"Error loading annotation files: {str(e)}")
 
113
 
114
  def get_organism_to_files(self) -> Dict[str, Dict[str, List[str]]]:
115
  """Get mapping of organisms to their files"""
116
+ return self._organism_to_files
117
 
118
  def get_organism_to_endonuclease(self) -> Dict[str, List[str]]:
119
  """Get mapping of organisms to their endonucleases"""
120
+ return self._organism_to_endonuclease
121
 
122
  def get_annotation_files(self) -> List[str]:
123
  """Get list of annotation files"""
124
+ return sorted(self._annotation_files, key=str.lower)
125
 
126
  def find_targets(self, input_data: dict) -> None:
127
  pass
src/models/NCBIWindowModel.py CHANGED
@@ -8,6 +8,7 @@ import os
8
  import platform
9
  import requests
10
  from urllib.parse import urlparse
 
11
 
12
  class NCBIWindowModel:
13
  class DownloadThread(QtCore.QThread):
@@ -26,7 +27,8 @@ class NCBIWindowModel:
26
  self.download_fna = download_fna
27
  self.download_gbff = download_gbff
28
  self.logger = controller.settings.get_logger()
29
- self.db_path = controller.settings.get_db_path()
 
30
 
31
  def run(self):
32
  try:
@@ -54,7 +56,7 @@ class NCBIWindowModel:
54
  file_type = 'FNA' if is_fna else 'GBFF'
55
  extension = '.gz' if is_gzipped else ''
56
 
57
- # Create output directory using the current database path
58
  output_dir = os.path.join(self.db_path, file_type)
59
  os.makedirs(output_dir, exist_ok=True)
60
 
@@ -129,6 +131,7 @@ class NCBIWindowModel:
129
  file_type = 'FNA' if is_fna else 'GBFF'
130
  extension = '.gz' if is_gzipped else ''
131
 
 
132
  local_filename = os.path.join(
133
  self.db_path,
134
  file_type,
@@ -185,6 +188,9 @@ class NCBIWindowModel:
185
  self.controller.model.add_downloaded_file(decompressed_filename)
186
 
187
  def __init__(self, settings):
 
 
 
188
  self.settings = settings
189
  self.logger = settings.get_logger()
190
  self.df = pd.DataFrame()
@@ -202,6 +208,8 @@ class NCBIWindowModel:
202
  "ENA (European Nucleotide Archive)": self._search_ena,
203
  "UCSC Genome Browser": self._search_ucsc
204
  }
 
 
205
 
206
  def search_ncbi(self, search_params):
207
  """Search selected database with given parameters"""
@@ -628,9 +636,20 @@ class NCBIWindowModel:
628
  raise
629
 
630
  def get_output_path(self, file_type):
631
- db_path = self.settings.get_db_path()
 
 
632
  self.logger.debug(f"Using database path for downloads: {db_path}")
633
- return os.path.join(db_path, file_type)
 
 
 
 
 
 
 
 
 
634
 
635
  def rename_file(self, old_name, new_name, file_type):
636
  old_path = os.path.join(self.get_output_path(file_type), old_name)
 
8
  import platform
9
  import requests
10
  from urllib.parse import urlparse
11
+ import time
12
 
13
  class NCBIWindowModel:
14
  class DownloadThread(QtCore.QThread):
 
27
  self.download_fna = download_fna
28
  self.download_gbff = download_gbff
29
  self.logger = controller.settings.get_logger()
30
+ self.db_path = controller.settings.db_manager.get_active_db_path()
31
+ self.logger.debug(f"Using database path for downloads: {self.db_path}")
32
 
33
  def run(self):
34
  try:
 
56
  file_type = 'FNA' if is_fna else 'GBFF'
57
  extension = '.gz' if is_gzipped else ''
58
 
59
+ # Create output directory using the active database path
60
  output_dir = os.path.join(self.db_path, file_type)
61
  os.makedirs(output_dir, exist_ok=True)
62
 
 
131
  file_type = 'FNA' if is_fna else 'GBFF'
132
  extension = '.gz' if is_gzipped else ''
133
 
134
+ # Use the active database path for FTP downloads as well
135
  local_filename = os.path.join(
136
  self.db_path,
137
  file_type,
 
188
  self.controller.model.add_downloaded_file(decompressed_filename)
189
 
190
  def __init__(self, settings):
191
+ start_time = time.time()
192
+ settings.logger.debug("Starting NCBIWindowModel initialization")
193
+
194
  self.settings = settings
195
  self.logger = settings.get_logger()
196
  self.df = pd.DataFrame()
 
208
  "ENA (European Nucleotide Archive)": self._search_ena,
209
  "UCSC Genome Browser": self._search_ucsc
210
  }
211
+
212
+ self.logger.debug(f"NCBIWindowModel initialization took: {time.time() - start_time:.2f} seconds")
213
 
214
  def search_ncbi(self, search_params):
215
  """Search selected database with given parameters"""
 
636
  raise
637
 
638
  def get_output_path(self, file_type):
639
+ """Get the appropriate output path for downloads"""
640
+ # Use the active database path which includes pending path during new genome analysis
641
+ db_path = self.settings.db_manager.get_active_db_path()
642
  self.logger.debug(f"Using database path for downloads: {db_path}")
643
+ output_path = os.path.join(db_path, file_type)
644
+ # Ensure the directory exists
645
+ os.makedirs(output_path, exist_ok=True)
646
+ return output_path
647
+
648
+ def cancel_pending_path(self):
649
+ """Cancel any pending path changes"""
650
+ if self.settings.db_manager.pending_db_path:
651
+ self.logger.info("Cancelling pending database path change")
652
+ self.settings.db_manager.pending_db_path = None
653
 
654
  def rename_file(self, old_name, new_name, file_type):
655
  old_path = os.path.join(self.get_output_path(file_type), old_name)
src/models/NewGenomeWindowModel.py CHANGED
@@ -64,13 +64,13 @@ class NewGenomeWindowModel(QObject):
64
  return False
65
 
66
  def create_arguments_command_for_job(self, organism_name, strain, organism_code, file_path, endonuclease_data, multithreading_checked, generate_repeats_checked):
67
- db_path = self.settings.get_db_path()
68
 
69
  # Ensure db_path ends with a forward slash
70
  if not db_path.endswith('/'):
71
  db_path = f"{db_path}/"
72
 
73
- self.logger.debug(f"Using database path: {db_path}") # Add logging
74
 
75
  print(f"The endonuclease data is {endonuclease_data}")
76
 
 
64
  return False
65
 
66
  def create_arguments_command_for_job(self, organism_name, strain, organism_code, file_path, endonuclease_data, multithreading_checked, generate_repeats_checked):
67
+ db_path = self.settings.db_manager.get_active_db_path()
68
 
69
  # Ensure db_path ends with a forward slash
70
  if not db_path.endswith('/'):
71
  db_path = f"{db_path}/"
72
 
73
+ self.logger.debug(f"Using database path for job processing: {db_path}") # Add logging
74
 
75
  print(f"The endonuclease data is {endonuclease_data}")
76
 
src/models/OffTarget/local_output.txt CHANGED
@@ -1,29 +1 @@
1
- DETAILED OUTPUT
2
- CACTTATGACCGGGCAACTT:0.000000
3
- ACACTTATGACCGGGCAACT:0.080598
4
- 0.080598,1,-100695,ACAATTACGCCCGGGCAACC
5
- TCAAAATAGCCCAAGTTGCC:0.000000
6
- ATTTTGCTACACTTATGACC:0.000000
7
- AATTTTGCTACACTTATGAC:0.000000
8
- GGGAATACTCCCTTTTATTG:0.000000
9
- GCAAAATTATCCTCAATAAA:0.018309
10
- 0.018309,1,1937923,GCAAAGTTTTCCTCAATATT
11
- CAAAATTATCCTCAATAAAA:0.107086
12
- 0.059761,1,2361124,CACATTTACCCTCAATGAAA
13
- 0.154411,1,4041223,CAAATCTATACTGAATAAAA
14
- CAGCTACAACCCGTGGCGGA:0.000000
15
- CCAGCTACAACCCGTGGCGG:0.106259
16
- 0.106259,1,4257161,ACAACTGCAAGCCGTGGCGG
17
- CCGCCAGCTACAACCCGTGG:0.000000
18
- AGGGAGTATTCCCTCCGCCA:0.000000
19
- GACCCGCCAGCTACAACCCG:0.000000
20
- GGGAGTATTCCCTCCGCCAC:0.000000
21
- CCTCCGCCACGGGTTGTAGC:0.129554
22
- 0.129554,1,-165804,GTTACGCCACGGGTTGTAGA
23
- CCGCCACGGGTTGTAGCTGG:0.000000
24
- CGCCACGGGTTGTAGCTGGC:0.000000
25
- GGACTACCAACGTTCACCAC:0.221431
26
- 0.234722,1,-469831,TCGCTACCATCGTTCACCAC
27
- 0.208141,1,2847166,GGAGAACCGACGGTCACCAC
28
- AGATAGTGTTCGTAATCCAG:0.000000
29
- CGTAATCCAGTGGTGAACGT:0.000000
 
1
+ AVG OUTPUT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/models/OffTargetModel.py CHANGED
@@ -105,6 +105,7 @@ class OffTargetModel(QObject):
105
  self.logger.debug(f"Max mismatches: {parameters['max_mismatches']}")
106
  self.logger.debug(f"Tolerance: {parameters['tolerance']}")
107
  self.logger.debug(f"Average output: {parameters['average_output']}")
 
108
 
109
  # Set working directory
110
  off_target_dir = self.global_settings.get_off_target_dir_path()
@@ -129,30 +130,8 @@ class OffTargetModel(QObject):
129
  self.logger.debug("Starting QProcess with command:")
130
  self.logger.debug(cmd)
131
 
132
-
133
- example_cmd = [
134
- "/Users/admin/Documents/proj/CASPERtest/CASPERapp/src/models/OffTarget/temp.txt",
135
- "spCas9",
136
- "/Users/admin/Documents/CASPERdb2/eck_12_spCas9.cspr",
137
- "/Users/admin/Documents/CASPERdb2/eck_12_spCas9_repeats.db",
138
- "/Users/admin/Documents/CASPERdb2/testtttt",
139
- "/Users/admin/Documents/proj/CASPERtest/CASPERapp/config/CASPERinfo",
140
- "4",
141
- "0.05",
142
- "FALSE",
143
- "TRUE",
144
- "MATRIX:HSU MATRIX-spCas9-2013"
145
- ]
146
-
147
- print(f"cmd: {cmd}")
148
-
149
- print(f"example_cmd: {example_cmd}")
150
-
151
-
152
  self.process.start(str(program_path), cmd)
153
 
154
-
155
-
156
  return True
157
 
158
  except Exception as e:
@@ -331,6 +310,21 @@ class OffTargetModel(QObject):
331
  casper_info_path = f'{self.global_settings.get_casper_info_path()}'
332
  endo = f'{parameters["endonuclease"]}'
333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  # Build command exactly as in old version
335
  cmd_parts = [
336
  temp_path,
@@ -343,7 +337,8 @@ class OffTargetModel(QObject):
343
  str(parameters['tolerance']),
344
  'FALSE' if parameters['average_output'] else 'TRUE',
345
  'TRUE' if parameters['average_output'] else 'FALSE',
346
- f'{self._get_hsu_value(parameters)}'
 
347
  ]
348
 
349
  return program_path, cmd_parts
 
105
  self.logger.debug(f"Max mismatches: {parameters['max_mismatches']}")
106
  self.logger.debug(f"Tolerance: {parameters['tolerance']}")
107
  self.logger.debug(f"Average output: {parameters['average_output']}")
108
+ self.logger.debug(f"Annotation file: {self.global_settings.get_current_annotation_file()}")
109
 
110
  # Set working directory
111
  off_target_dir = self.global_settings.get_off_target_dir_path()
 
130
  self.logger.debug("Starting QProcess with command:")
131
  self.logger.debug(cmd)
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  self.process.start(str(program_path), cmd)
134
 
 
 
135
  return True
136
 
137
  except Exception as e:
 
310
  casper_info_path = f'{self.global_settings.get_casper_info_path()}'
311
  endo = f'{parameters["endonuclease"]}'
312
 
313
+ # Get annotation file path
314
+ annotation_file = self.global_settings.get_current_annotation_file()
315
+ if not annotation_file:
316
+ raise ValueError("No annotation file selected")
317
+
318
+ # Build full annotation path and verify it exists
319
+ annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
320
+ if not os.path.isfile(annotation_path):
321
+ # Try without GBFF subdirectory
322
+ annotation_path = os.path.join(self.global_settings.get_db_path(), annotation_file)
323
+ if not os.path.isfile(annotation_path):
324
+ raise ValueError(f"Annotation file not found at {annotation_path}")
325
+
326
+ self.logger.debug(f"Using annotation file: {annotation_path}")
327
+
328
  # Build command exactly as in old version
329
  cmd_parts = [
330
  temp_path,
 
337
  str(parameters['tolerance']),
338
  'FALSE' if parameters['average_output'] else 'TRUE',
339
  'TRUE' if parameters['average_output'] else 'FALSE',
340
+ f'{self._get_hsu_value(parameters)}',
341
+ annotation_path # Add annotation file path
342
  ]
343
 
344
  return program_path, cmd_parts
src/models/PopulationAnalysisWindowModel.py CHANGED
@@ -19,11 +19,8 @@ class PopulationAnalysisWindowModel:
19
  def load_endonucleases(self):
20
  """Load endonucleases from GlobalSettings"""
21
  try:
22
- self.logger.info("Starting load_endonucleases()")
23
-
24
  # Get endonucleases from global settings
25
  endos = self.settings.get_endonucleases()
26
- self.logger.debug(f"Raw endonucleases from settings: {endos}")
27
 
28
  if not endos:
29
  self.logger.warning("No endonucleases returned from settings")
@@ -32,7 +29,6 @@ class PopulationAnalysisWindowModel:
32
  # Format the endonucleases for display
33
  formatted_endos = {}
34
  for endo, data in endos.items():
35
- self.logger.debug(f"Processing endo: {endo}, data: {data}")
36
  pam = data.get('pam', '').strip()
37
  # Remove any extra "PAM:" text that might be in the PAM string
38
  pam = pam.replace('PAM:', '').strip()
@@ -44,7 +40,6 @@ class PopulationAnalysisWindowModel:
44
  data.get('default_seed_length', ''),
45
  data.get('default_three_length', ''))
46
 
47
- self.logger.info(f"Successfully formatted {len(formatted_endos)} endonucleases")
48
  self.logger.debug(f"Formatted endonucleases: {formatted_endos}")
49
  return formatted_endos
50
 
 
19
  def load_endonucleases(self):
20
  """Load endonucleases from GlobalSettings"""
21
  try:
 
 
22
  # Get endonucleases from global settings
23
  endos = self.settings.get_endonucleases()
 
24
 
25
  if not endos:
26
  self.logger.warning("No endonucleases returned from settings")
 
29
  # Format the endonucleases for display
30
  formatted_endos = {}
31
  for endo, data in endos.items():
 
32
  pam = data.get('pam', '').strip()
33
  # Remove any extra "PAM:" text that might be in the PAM string
34
  pam = pam.replace('PAM:', '').strip()
 
40
  data.get('default_seed_length', ''),
41
  data.get('default_three_length', ''))
42
 
 
43
  self.logger.debug(f"Formatted endonucleases: {formatted_endos}")
44
  return formatted_endos
45
 
src/models/StartupWindowModel.py CHANGED
@@ -2,6 +2,7 @@ from PyQt6.QtCore import QObject, pyqtSignal
2
 
3
  class StartupWindowModel(QObject):
4
  db_state_updated = pyqtSignal(bool, str, list)
 
5
 
6
  def __init__(self, global_settings):
7
  super().__init__()
@@ -12,16 +13,32 @@ class StartupWindowModel(QObject):
12
  self.settings.db_manager.db_state_changed.connect(self.on_db_state_updated)
13
 
14
  def get_db_path(self):
 
15
  return self.settings.get_db_path()
16
 
17
- def save_db_path(self, directory_path):
18
- """Save the database path and trigger validation"""
19
- self.logger.debug(f"Saving database path: {directory_path}")
20
- # The db_manager will emit its own signals that we're now listening to
21
- success, message = self.settings.save_db_path(directory_path)
22
- return success, message
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  def on_db_state_updated(self, is_valid, message, cspr_files):
25
  """Handle database state updates"""
26
- self.logger.debug(f"StartupWindowModel received db state update: valid={is_valid}, message={message}, cspr_files_count={len(cspr_files)}")
 
 
 
27
  self.db_state_updated.emit(is_valid, message, cspr_files)
 
2
 
3
  class StartupWindowModel(QObject):
4
  db_state_updated = pyqtSignal(bool, str, list)
5
+ _is_saving = False # Add flag to prevent recursion
6
 
7
  def __init__(self, global_settings):
8
  super().__init__()
 
13
  self.settings.db_manager.db_state_changed.connect(self.on_db_state_updated)
14
 
15
  def get_db_path(self):
16
+ """Get the current database path without modifying it"""
17
  return self.settings.get_db_path()
18
 
19
+ def save_db_path(self, path):
20
+ """Save the database path"""
21
+ try:
22
+ if self._is_saving: # Prevent recursive saves
23
+ return
24
+
25
+ self._is_saving = True
26
+ try:
27
+ # Don't clear the path if it's invalid - let the controller handle that
28
+ self.settings.save_db_path(path)
29
+ self.settings.update_db_state()
30
+ finally:
31
+ self._is_saving = False
32
+
33
+ except Exception as e:
34
+ self._is_saving = False
35
+ self.logger.error(f"Error saving database path: {str(e)}")
36
+ raise
37
 
38
  def on_db_state_updated(self, is_valid, message, cspr_files):
39
  """Handle database state updates"""
40
+ if self._is_saving: # Don't emit signals during save operation
41
+ return
42
+
43
+ self.logger.debug(f"StartupWindowModel received db state update: valid={is_valid}, message={message}")
44
  self.db_state_updated.emit(is_valid, message, cspr_files)
src/models/ViewTargetsModel.py CHANGED
@@ -1,88 +1,67 @@
1
  from models.CSPRparser import CSPRparser
2
- from models.HomeWindowModel import HomeWindowModel
3
- from models.AnnotationParser import AnnotationParser
4
- import os
5
  from Bio import SeqIO
6
  from collections import defaultdict
7
  import traceback
 
8
 
9
- class ViewTargetsModel(HomeWindowModel):
10
  def __init__(self, global_settings):
11
  super().__init__(global_settings)
 
 
12
  self.guides = []
13
  self.cspr_parser = None
14
- self.annotation_parser = None
15
  self.gene_sequence = ""
16
  self.highlighted_sequence = ""
17
  self.gene_info = {}
18
  self.available_genes = []
19
  self.filter_options = {}
20
  self.scoring_options = {}
21
- self.annotation_path = ""
22
  self.current_gene_start = 0
23
  self.current_gene_end = 0
24
  self.extended_sequence = ""
25
  self.chromosome = ""
26
 
 
27
  self._gene_data_cache = {}
28
  self._sequence_cache = {}
29
  self._parser_cache = {}
30
  self._chromosome_seqs = {}
31
  self._cached_guides = {}
32
 
33
- self.global_settings.annotation_file_changed.connect(self._on_annotation_file_changed)
 
 
 
 
 
 
34
 
35
- # Initialize annotation path
36
- self.annotation_path = os.path.join(
37
- self.global_settings.get_db_path(),
38
- 'GBFF',
39
- self.global_settings.get_current_annotation_file()
40
- )
41
- self.logger.debug(f"Initialized annotation path: {self.annotation_path}")
42
-
43
- def cleanup(self):
44
- """Cleanup method to be called when the view is closed"""
45
- try:
46
- # Disconnect from annotation file changes
47
- if hasattr(self, '_annotation_signal'):
48
- self.global_settings.annotation_file_changed.disconnect(self._on_annotation_file_changed)
49
- self.global_settings.logger.debug("ViewTargetsModel disconnected from annotation file changes")
50
-
51
- self._gene_data_cache.clear()
52
- self._sequence_cache.clear()
53
- self._parser_cache.clear()
54
-
55
- except Exception as e:
56
- self.global_settings.logger.error(f"Error in ViewTargetsModel cleanup: {str(e)}")
57
 
58
- def _on_annotation_file_changed(self, new_annotation_file):
59
- """Clear all caches when annotation file changes"""
60
- try:
61
- self.logger.debug(f"ViewTargetsModel clearing caches for new annotation file: {new_annotation_file}")
62
- self._gene_data_cache.clear()
63
- self._sequence_cache.clear()
64
- self._parser_cache.clear()
65
-
66
- # Update annotation path and parser
67
- self.annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', new_annotation_file)
68
- self.annotation_parser = AnnotationParser(self.global_settings)
69
- self.annotation_parser.set_annotation_file(self.annotation_path)
70
-
71
- # Clear other stored data
72
- self.gene_sequence = ""
73
- self.highlighted_sequence = ""
74
- self.gene_info = {}
75
- self.available_genes = []
76
- self._chromosome_seqs = {}
77
-
78
- except Exception as e:
79
- self.logger.error(f"Error in _on_annotation_file_changed: {str(e)}")
80
 
81
  def load_guides(self, selected_targets, organism, endonuclease):
82
  """Load guides with proper error handling"""
83
  try:
84
- self.logger.debug(f"Starting load_guides with {len(selected_targets)} targets")
85
-
86
  self.organism = organism
87
  self.endonuclease = endonuclease
88
 
@@ -92,7 +71,7 @@ class ViewTargetsModel(HomeWindowModel):
92
  self.cspr_parser = self._parser_cache[cspr_key]
93
  self.logger.debug("Using cached CSPR parser")
94
  else:
95
- org_files = self.get_organism_to_files()
96
  if organism not in org_files or endonuclease not in org_files[organism]:
97
  self.logger.error(f"No CSPR file found for {organism} and {endonuclease}")
98
  return
@@ -101,7 +80,6 @@ class ViewTargetsModel(HomeWindowModel):
101
  cspr_path = os.path.join(self.global_settings.get_db_path(), cspr_file)
102
  self.cspr_parser = CSPRparser(cspr_path, self.global_settings.get_casper_info_path())
103
  self._parser_cache[cspr_key] = self.cspr_parser
104
- self.logger.debug("Created new CSPR parser")
105
 
106
  # Initialize guides and genes
107
  self.guides = []
@@ -156,8 +134,6 @@ class ViewTargetsModel(HomeWindowModel):
156
  self.guides = list(unique_guides.values())
157
 
158
  self.logger.debug(f"Found {len(self.guides)} unique guides")
159
- self.logger.debug(f"Available genes: {self.available_genes}")
160
-
161
  except Exception as e:
162
  self.logger.error(f"Error in load_guides: {str(e)}")
163
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
@@ -174,13 +150,6 @@ class ViewTargetsModel(HomeWindowModel):
174
 
175
  return self._chromosome_seqs.get(chromosome)
176
 
177
- def _initialize_annotation_parser(self):
178
- """Initialize annotation parser if not already initialized"""
179
- if self.annotation_parser is None:
180
- self.annotation_parser = AnnotationParser(self.global_settings)
181
- if self.annotation_path:
182
- self.annotation_parser.set_annotation_file(self.annotation_path)
183
-
184
  def get_gene_data(self, locus_tag):
185
  """Get gene data with proper error handling"""
186
  try:
@@ -192,25 +161,19 @@ class ViewTargetsModel(HomeWindowModel):
192
  if locus_tag in self._gene_data_cache:
193
  return self._gene_data_cache[locus_tag]
194
 
195
- # Initialize annotation parser if not already done
196
- if not hasattr(self, 'annotation_parser') or self.annotation_parser is None:
197
- self.annotation_parser = AnnotationParser(self.global_settings)
198
- annotation_file = self.global_settings.get_current_annotation_file()
199
- annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
200
- self.annotation_parser.set_annotation_file(annotation_path)
201
- self.logger.debug(f"Initialized annotation parser with file: {annotation_path}")
202
 
203
  # Get gene data from parser with proper string conversion
204
  gene_data = None
205
  if isinstance(locus_tag, (str, int)):
206
  locus_tag_str = str(locus_tag).strip()
207
- self.logger.debug(f"Searching for locus tag: {locus_tag_str}")
208
  # Look up by locus tag directly
209
  gene_data = self.annotation_parser.get_gene_data(locus_tag_str.lower())
210
 
211
  if gene_data:
212
  self._gene_data_cache[locus_tag] = gene_data
213
- self.logger.debug(f"Found gene data: {gene_data.keys()}")
214
  else:
215
  self.logger.debug(f"No gene data found for locus tag: {locus_tag}")
216
 
@@ -244,7 +207,6 @@ class ViewTargetsModel(HomeWindowModel):
244
  self.logger.debug(f"View exons only is: {getattr(self, '_view_exons_only', False)}")
245
 
246
  # Regular gene-based search
247
- self.logger.debug(f"Getting gene data for locus tag: {identifier}")
248
  gene_data = self.get_gene_data(identifier)
249
  if not gene_data or 'info' not in gene_data:
250
  self.logger.warning(f"No gene data found for locus tag: {identifier}")
@@ -255,53 +217,75 @@ class ViewTargetsModel(HomeWindowModel):
255
  print(f"gene_data: {gene_data}")
256
  full_location = gene_data['info'].get('full_location', '')
257
  print(f"Full location: {full_location}")
258
- if full_location and ',' in full_location: # Multiple exons
259
- self.logger.debug(f"Processing exons from full location: {full_location}")
260
- exon_sequences = []
261
- full_sequence = gene_data['sequence'] # Use sequence from gene_data
262
-
263
- # Calculate padding offset
264
- padding = 30
265
- gene_start = gene_data['info']['start']
266
- padded_start = max(0, gene_start - padding)
267
- padding_offset = gene_start - padded_start
268
-
269
- print(f"gene_start: {gene_start}, padded_start: {padded_start}, padding_offset: {padding_offset}")
270
-
271
- # Process each exon location
272
- for exon in full_location.split(','):
273
- # Extract coordinates and strand
274
- coords = exon.split('(')[0] # Get part before strand
275
- strand = exon.split('(')[1][0] # Get + or - from (+ or (-
276
- start, end = map(int, coords.split('..'))
 
277
 
278
- print(f"coords: {coords}, strand: {strand}, start: {start}, end: {end}")
 
 
 
 
 
 
 
 
 
 
279
 
280
- # Adjust coordinates relative to gene start and account for padding
281
- relative_start = start - gene_start + padding_offset
282
- relative_end = end - gene_start + padding_offset
283
- print(f"relative_start: {relative_start}, relative_end: {relative_end}")
284
 
285
- # Get exon sequence from the padded sequence
286
- exon_seq = full_sequence[relative_start:relative_end]
 
 
 
 
 
 
 
 
 
287
 
288
- exon_sequences.append(exon_seq)
289
-
290
- # Join exon sequences
291
- sequence = ''.join(exon_sequences)
292
- self.logger.debug(f"Created concatenated exon sequence of length: {len(sequence)}")
293
-
294
- return {
295
- 'sequence': sequence,
296
- 'info': gene_data['info'],
297
- 'start': gene_data['info']['start'],
298
- 'end': gene_data['info']['end']
299
- }
 
 
 
 
300
 
301
  # If not in exons-only mode or no exons to process, return normal sequence
302
  if 'sequence' in gene_data:
303
  sequence = gene_data['sequence']
304
- self.logger.debug(f"Got sequence of length: {len(sequence)}")
305
 
306
  # Format sequence with padding in lowercase (only if not in exons-only mode)
307
  if not hasattr(self, '_view_exons_only') or not self._view_exons_only:
@@ -328,8 +312,6 @@ class ViewTargetsModel(HomeWindowModel):
328
  'end': gene_data['info']['end'],
329
  'full_location': gene_data['info'].get('full_location', '')
330
  }
331
-
332
- self.logger.debug(f"Returning sequence of length: {len(formatted_sequence)}")
333
  return result
334
 
335
  self.logger.warning(f"No sequence data found in gene_data for {identifier}")
@@ -343,27 +325,25 @@ class ViewTargetsModel(HomeWindowModel):
343
  def _get_sequence_for_position(self, chrom, start, end):
344
  """Get sequence for a given position with proper padding handling"""
345
  try:
346
- if not hasattr(self, 'annotation_parser') or self.annotation_parser is None:
347
- self.annotation_parser = AnnotationParser(self.global_settings)
348
- annotation_file = self.global_settings.get_current_annotation_file()
349
- annotation_path = os.path.join(self.global_settings.get_db_path(), 'GBFF', annotation_file)
350
- self.annotation_parser.set_annotation_file(annotation_path)
351
 
352
  feature_info = {
353
  'chromosome': chrom, # Use raw chromosome ID directly
354
- 'start': start-1, # Convert to 0-based indexing
355
  'end': end
356
  }
357
 
358
  self.logger.debug(f"Getting sequence for feature info: {feature_info}")
359
 
360
- sequence = self.annotation_parser._get_sequence_for_gene(feature_info)
 
361
  if sequence:
362
  padding = 30
363
 
364
  # Handle start position padding
365
- if start == 1:
366
- # No padding at start if starting at position 1
367
  five_prime_pad = ""
368
  main_sequence = sequence[:-(padding if len(sequence) > padding else 0)].upper()
369
  else:
 
1
  from models.CSPRparser import CSPRparser
2
+ from models.BaseModel import BaseModel
 
 
3
  from Bio import SeqIO
4
  from collections import defaultdict
5
  import traceback
6
+ import os
7
 
8
+ class ViewTargetsModel(BaseModel):
9
  def __init__(self, global_settings):
10
  super().__init__(global_settings)
11
+
12
+ # Initialize model state
13
  self.guides = []
14
  self.cspr_parser = None
 
15
  self.gene_sequence = ""
16
  self.highlighted_sequence = ""
17
  self.gene_info = {}
18
  self.available_genes = []
19
  self.filter_options = {}
20
  self.scoring_options = {}
 
21
  self.current_gene_start = 0
22
  self.current_gene_end = 0
23
  self.extended_sequence = ""
24
  self.chromosome = ""
25
 
26
+ # Initialize caches
27
  self._gene_data_cache = {}
28
  self._sequence_cache = {}
29
  self._parser_cache = {}
30
  self._chromosome_seqs = {}
31
  self._cached_guides = {}
32
 
33
+ def _clear_caches(self):
34
+ """Clear all model-specific caches"""
35
+ self._gene_data_cache.clear()
36
+ self._sequence_cache.clear()
37
+ self._parser_cache.clear()
38
+ self._chromosome_seqs.clear()
39
+ self._cached_guides.clear()
40
 
41
+ # Clear other stored data
42
+ self.gene_sequence = ""
43
+ self.highlighted_sequence = ""
44
+ self.gene_info = {}
45
+ self.available_genes = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ def _ensure_annotation_parser(self) -> bool:
48
+ """Ensure annotation parser is initialized
49
+
50
+ Returns:
51
+ bool: True if parser is ready, False otherwise
52
+ """
53
+ if self.annotation_parser is None:
54
+ try:
55
+ self._initialize_annotation_parser()
56
+ return True
57
+ except Exception as e:
58
+ self.logger.error(f"Failed to initialize annotation parser: {str(e)}")
59
+ return False
60
+ return True
 
 
 
 
 
 
 
 
61
 
62
  def load_guides(self, selected_targets, organism, endonuclease):
63
  """Load guides with proper error handling"""
64
  try:
 
 
65
  self.organism = organism
66
  self.endonuclease = endonuclease
67
 
 
71
  self.cspr_parser = self._parser_cache[cspr_key]
72
  self.logger.debug("Using cached CSPR parser")
73
  else:
74
+ org_files = self.global_settings.get_organism_files()
75
  if organism not in org_files or endonuclease not in org_files[organism]:
76
  self.logger.error(f"No CSPR file found for {organism} and {endonuclease}")
77
  return
 
80
  cspr_path = os.path.join(self.global_settings.get_db_path(), cspr_file)
81
  self.cspr_parser = CSPRparser(cspr_path, self.global_settings.get_casper_info_path())
82
  self._parser_cache[cspr_key] = self.cspr_parser
 
83
 
84
  # Initialize guides and genes
85
  self.guides = []
 
134
  self.guides = list(unique_guides.values())
135
 
136
  self.logger.debug(f"Found {len(self.guides)} unique guides")
 
 
137
  except Exception as e:
138
  self.logger.error(f"Error in load_guides: {str(e)}")
139
  self.logger.error(f"Stack trace: {traceback.format_exc()}")
 
150
 
151
  return self._chromosome_seqs.get(chromosome)
152
 
 
 
 
 
 
 
 
153
  def get_gene_data(self, locus_tag):
154
  """Get gene data with proper error handling"""
155
  try:
 
161
  if locus_tag in self._gene_data_cache:
162
  return self._gene_data_cache[locus_tag]
163
 
164
+ # Ensure parser is initialized
165
+ if not self._ensure_annotation_parser():
166
+ return None
 
 
 
 
167
 
168
  # Get gene data from parser with proper string conversion
169
  gene_data = None
170
  if isinstance(locus_tag, (str, int)):
171
  locus_tag_str = str(locus_tag).strip()
 
172
  # Look up by locus tag directly
173
  gene_data = self.annotation_parser.get_gene_data(locus_tag_str.lower())
174
 
175
  if gene_data:
176
  self._gene_data_cache[locus_tag] = gene_data
 
177
  else:
178
  self.logger.debug(f"No gene data found for locus tag: {locus_tag}")
179
 
 
207
  self.logger.debug(f"View exons only is: {getattr(self, '_view_exons_only', False)}")
208
 
209
  # Regular gene-based search
 
210
  gene_data = self.get_gene_data(identifier)
211
  if not gene_data or 'info' not in gene_data:
212
  self.logger.warning(f"No gene data found for locus tag: {identifier}")
 
217
  print(f"gene_data: {gene_data}")
218
  full_location = gene_data['info'].get('full_location', '')
219
  print(f"Full location: {full_location}")
220
+ if full_location:
221
+ if ',' in full_location: # Multiple exons
222
+ self.logger.debug(f"Processing exons from full location: {full_location}")
223
+ exon_sequences = []
224
+ full_sequence = gene_data['sequence'] # Use sequence from gene_data
225
+
226
+ # Calculate padding offset
227
+ padding = 30
228
+ gene_start = gene_data['info']['start']
229
+ padded_start = max(0, gene_start - padding)
230
+ padding_offset = gene_start - padded_start
231
+
232
+ print(f"gene_start: {gene_start}, padded_start: {padded_start}, padding_offset: {padding_offset}")
233
+
234
+ # Process each exon location
235
+ for exon in full_location.split(','):
236
+ # Extract coordinates and strand
237
+ coords = exon.split('(')[0] # Get part before strand
238
+ strand = exon.split('(')[1][0] # Get + or - from (+ or (-
239
+ start, end = map(int, coords.split('..'))
240
 
241
+ print(f"coords: {coords}, strand: {strand}, start: {start}, end: {end}")
242
+
243
+ # Adjust coordinates relative to gene start and account for padding
244
+ relative_start = start - gene_start + padding_offset
245
+ relative_end = end - gene_start + padding_offset
246
+ print(f"relative_start: {relative_start}, relative_end: {relative_end}")
247
+
248
+ # Get exon sequence from the padded sequence
249
+ exon_seq = full_sequence[relative_start:relative_end]
250
+
251
+ exon_sequences.append(exon_seq)
252
 
253
+ # Join exon sequences
254
+ sequence = ''.join(exon_sequences)
255
+ self.logger.debug(f"Created concatenated exon sequence of length: {len(sequence)}")
 
256
 
257
+ return {
258
+ 'sequence': sequence,
259
+ 'info': gene_data['info'],
260
+ 'start': gene_data['info']['start'],
261
+ 'end': gene_data['info']['end']
262
+ }
263
+ else: # Single location/exon
264
+ # Return sequence without padding for single exon
265
+ sequence = gene_data['sequence']
266
+ gene_start = gene_data['info']['start']
267
+ gene_end = gene_data['info']['end']
268
 
269
+ # Calculate padding offset
270
+ padding = 30
271
+ padded_start = max(0, gene_start - padding)
272
+ padding_offset = gene_start - padded_start
273
+
274
+ # Get sequence without padding
275
+ relative_start = padding_offset
276
+ relative_end = len(sequence) - padding_offset
277
+ sequence = sequence[relative_start:relative_end]
278
+
279
+ return {
280
+ 'sequence': sequence,
281
+ 'info': gene_data['info'],
282
+ 'start': gene_data['info']['start'],
283
+ 'end': gene_data['info']['end']
284
+ }
285
 
286
  # If not in exons-only mode or no exons to process, return normal sequence
287
  if 'sequence' in gene_data:
288
  sequence = gene_data['sequence']
 
289
 
290
  # Format sequence with padding in lowercase (only if not in exons-only mode)
291
  if not hasattr(self, '_view_exons_only') or not self._view_exons_only:
 
312
  'end': gene_data['info']['end'],
313
  'full_location': gene_data['info'].get('full_location', '')
314
  }
 
 
315
  return result
316
 
317
  self.logger.warning(f"No sequence data found in gene_data for {identifier}")
 
325
  def _get_sequence_for_position(self, chrom, start, end):
326
  """Get sequence for a given position with proper padding handling"""
327
  try:
328
+ if not self._ensure_annotation_parser():
329
+ return None
 
 
 
330
 
331
  feature_info = {
332
  'chromosome': chrom, # Use raw chromosome ID directly
333
+ 'start': start, # Keep as is since annotation parser handles 0-based conversion
334
  'end': end
335
  }
336
 
337
  self.logger.debug(f"Getting sequence for feature info: {feature_info}")
338
 
339
+ # Use annotation parser's method directly
340
+ sequence = self.annotation_parser._get_sequence_for_position(chrom, start, end)
341
  if sequence:
342
  padding = 30
343
 
344
  # Handle start position padding
345
+ if start == 0: # Already 0-based for sequence operations
346
+ # No padding at start if starting at position 0
347
  five_prime_pad = ""
348
  main_sequence = sequence[:-(padding if len(sequence) > padding else 0)].upper()
349
  else:
src/ui/find_targets.ui CHANGED
@@ -55,31 +55,6 @@
55
  </item>
56
  <item row="3" column="0" rowspan="2" colspan="2">
57
  <layout class="QHBoxLayout" name="horizontalLayout">
58
- <item alignment="Qt::AlignLeft">
59
- <widget class="QPushButton" name="pbtnBack">
60
- <property name="sizePolicy">
61
- <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
62
- <horstretch>0</horstretch>
63
- <verstretch>0</verstretch>
64
- </sizepolicy>
65
- </property>
66
- <property name="minimumSize">
67
- <size>
68
- <width>125</width>
69
- <height>0</height>
70
- </size>
71
- </property>
72
- <property name="maximumSize">
73
- <size>
74
- <width>16777215</width>
75
- <height>16777215</height>
76
- </size>
77
- </property>
78
- <property name="text">
79
- <string>Return</string>
80
- </property>
81
- </widget>
82
- </item>
83
  <item>
84
  <widget class="QPushButton" name="pbtnGenerateLibrary">
85
  <property name="sizePolicy">
 
55
  </item>
56
  <item row="3" column="0" rowspan="2" colspan="2">
57
  <layout class="QHBoxLayout" name="horizontalLayout">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  <item>
59
  <widget class="QPushButton" name="pbtnGenerateLibrary">
60
  <property name="sizePolicy">
src/ui/home_window.ui CHANGED
@@ -10,7 +10,7 @@
10
  <x>0</x>
11
  <y>0</y>
12
  <width>960</width>
13
- <height>1147</height>
14
  </rect>
15
  </property>
16
  <property name="font">
@@ -50,7 +50,50 @@
50
  <string notr="true"/>
51
  </property>
52
  <layout class="QGridLayout" name="gridContainer">
53
- <item row="4" column="0" rowspan="2" colspan="2">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  <layout class="QGridLayout" name="gridStep1Step2Step3">
55
  <property name="sizeConstraint">
56
  <enum>QLayout::SetDefaultConstraint</enum>
@@ -551,112 +594,6 @@
551
  </item>
552
  </layout>
553
  </item>
554
- <item row="0" column="0">
555
- <widget class="QLabel" name="lblWindowHeading">
556
- <property name="sizePolicy">
557
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
558
- <horstretch>0</horstretch>
559
- <verstretch>0</verstretch>
560
- </sizepolicy>
561
- </property>
562
- <property name="minimumSize">
563
- <size>
564
- <width>0</width>
565
- <height>0</height>
566
- </size>
567
- </property>
568
- <property name="maximumSize">
569
- <size>
570
- <width>16777215</width>
571
- <height>16777215</height>
572
- </size>
573
- </property>
574
- <property name="font">
575
- <font>
576
- <family>Arial</family>
577
- <pointsize>12</pointsize>
578
- <weight>75</weight>
579
- <italic>false</italic>
580
- <bold>true</bold>
581
- </font>
582
- </property>
583
- <property name="styleSheet">
584
- <string notr="true"/>
585
- </property>
586
- <property name="text">
587
- <string>CASPER</string>
588
- </property>
589
- </widget>
590
- </item>
591
- <item row="0" column="1">
592
- <widget class="QLabel" name="pbtnThemeToggle">
593
- <property name="minimumSize">
594
- <size>
595
- <width>50</width>
596
- <height>0</height>
597
- </size>
598
- </property>
599
- <property name="maximumSize">
600
- <size>
601
- <width>50</width>
602
- <height>16777215</height>
603
- </size>
604
- </property>
605
- <property name="text">
606
- <string/>
607
- </property>
608
- </widget>
609
- </item>
610
- <item row="2" column="0" colspan="2">
611
- <widget class="QGroupBox" name="grpNavigationMenu">
612
- <property name="sizePolicy">
613
- <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
614
- <horstretch>0</horstretch>
615
- <verstretch>0</verstretch>
616
- </sizepolicy>
617
- </property>
618
- <property name="title">
619
- <string>CASPER Navigation</string>
620
- </property>
621
- <layout class="QGridLayout" name="gridLayout_2">
622
- <item row="1" column="1">
623
- <widget class="QPushButton" name="pbtnPopulationAnalysis">
624
- <property name="text">
625
- <string>Population Analysis</string>
626
- </property>
627
- </widget>
628
- </item>
629
- <item row="0" column="1">
630
- <widget class="QPushButton" name="pbtnNewEndonuclease">
631
- <property name="text">
632
- <string>Define New Endonuclease</string>
633
- </property>
634
- </widget>
635
- </item>
636
- <item row="1" column="0">
637
- <widget class="QPushButton" name="pbtnMultitargetingAnalysis">
638
- <property name="text">
639
- <string>Multitargeting Analysis</string>
640
- </property>
641
- </widget>
642
- </item>
643
- <item row="2" column="0">
644
- <widget class="QPushButton" name="pbtnCombineFiles">
645
- <property name="text">
646
- <string>Combine Files</string>
647
- </property>
648
- </widget>
649
- </item>
650
- <item row="0" column="0">
651
- <widget class="QPushButton" name="pbtnNewGenome">
652
- <property name="text">
653
- <string>Analyze New Genome</string>
654
- </property>
655
- </widget>
656
- </item>
657
- </layout>
658
- </widget>
659
- </item>
660
  </layout>
661
  </widget>
662
  <action name="actionNew">
 
10
  <x>0</x>
11
  <y>0</y>
12
  <width>960</width>
13
+ <height>916</height>
14
  </rect>
15
  </property>
16
  <property name="font">
 
50
  <string notr="true"/>
51
  </property>
52
  <layout class="QGridLayout" name="gridContainer">
53
+ <item row="1" column="0" colspan="2">
54
+ <widget class="QGroupBox" name="grpNavigationMenu">
55
+ <property name="sizePolicy">
56
+ <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
57
+ <horstretch>0</horstretch>
58
+ <verstretch>0</verstretch>
59
+ </sizepolicy>
60
+ </property>
61
+ <property name="title">
62
+ <string>CASPER Navigation</string>
63
+ </property>
64
+ <layout class="QGridLayout" name="gridLayout_2">
65
+ <item row="1" column="1">
66
+ <widget class="QPushButton" name="pbtnPopulationAnalysis">
67
+ <property name="text">
68
+ <string>Population Analysis</string>
69
+ </property>
70
+ </widget>
71
+ </item>
72
+ <item row="0" column="1">
73
+ <widget class="QPushButton" name="pbtnNewEndonuclease">
74
+ <property name="text">
75
+ <string>Define New Endonuclease</string>
76
+ </property>
77
+ </widget>
78
+ </item>
79
+ <item row="0" column="0">
80
+ <widget class="QPushButton" name="pbtnNewGenome">
81
+ <property name="text">
82
+ <string>Analyze New Genome</string>
83
+ </property>
84
+ </widget>
85
+ </item>
86
+ <item row="1" column="0">
87
+ <widget class="QPushButton" name="pbtnMultitargetingAnalysis">
88
+ <property name="text">
89
+ <string>Multitargeting Analysis</string>
90
+ </property>
91
+ </widget>
92
+ </item>
93
+ </layout>
94
+ </widget>
95
+ </item>
96
+ <item row="3" column="0" rowspan="2" colspan="2">
97
  <layout class="QGridLayout" name="gridStep1Step2Step3">
98
  <property name="sizeConstraint">
99
  <enum>QLayout::SetDefaultConstraint</enum>
 
594
  </item>
595
  </layout>
596
  </item>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  </layout>
598
  </widget>
599
  <action name="actionNew">
src/ui/main_window.ui CHANGED
@@ -71,7 +71,7 @@
71
  <x>0</x>
72
  <y>0</y>
73
  <width>901</width>
74
- <height>37</height>
75
  </rect>
76
  </property>
77
  <property name="font">
@@ -83,28 +83,6 @@
83
  <bold>false</bold>
84
  </font>
85
  </property>
86
- <widget class="QMenu" name="menuFile">
87
- <property name="title">
88
- <string>File</string>
89
- </property>
90
- <addaction name="actionExit"/>
91
- <addaction name="actChangeDatabaseDirectory"/>
92
- </widget>
93
- <widget class="QMenu" name="menuTools">
94
- <property name="title">
95
- <string>Tools</string>
96
- </property>
97
- <addaction name="actOpenGenomeBrowser"/>
98
- </widget>
99
- <widget class="QMenu" name="menuLinks">
100
- <property name="title">
101
- <string>Links</string>
102
- </property>
103
- <addaction name="actGoToNCBI"/>
104
- <addaction name="actionGoToNCBIBLAST"/>
105
- <addaction name="separator"/>
106
- <addaction name="actionGoToCASPERRepository"/>
107
- </widget>
108
  <widget class="QMenu" name="menuCASPER">
109
  <property name="title">
110
  <string>CASPER</string>
@@ -112,9 +90,6 @@
112
  <addaction name="actionAbout_CASPER"/>
113
  </widget>
114
  <addaction name="menuCASPER"/>
115
- <addaction name="menuFile"/>
116
- <addaction name="menuTools"/>
117
- <addaction name="menuLinks"/>
118
  </widget>
119
  <widget class="QToolBar" name="toolBar">
120
  <property name="windowTitle">
 
71
  <x>0</x>
72
  <y>0</y>
73
  <width>901</width>
74
+ <height>24</height>
75
  </rect>
76
  </property>
77
  <property name="font">
 
83
  <bold>false</bold>
84
  </font>
85
  </property>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  <widget class="QMenu" name="menuCASPER">
87
  <property name="title">
88
  <string>CASPER</string>
 
90
  <addaction name="actionAbout_CASPER"/>
91
  </widget>
92
  <addaction name="menuCASPER"/>
 
 
 
93
  </widget>
94
  <widget class="QToolBar" name="toolBar">
95
  <property name="windowTitle">
src/ui/new_genome_window.ui CHANGED
@@ -502,40 +502,6 @@ search parameters</string>
502
  </item>
503
  </layout>
504
  </item>
505
- <item row="0" column="1">
506
- <widget class="QLabel" name="lblWindowHeading">
507
- <property name="minimumSize">
508
- <size>
509
- <width>0</width>
510
- <height>0</height>
511
- </size>
512
- </property>
513
- <property name="maximumSize">
514
- <size>
515
- <width>16777215</width>
516
- <height>50</height>
517
- </size>
518
- </property>
519
- <property name="font">
520
- <font>
521
- <family>Arial</family>
522
- <pointsize>12</pointsize>
523
- <weight>75</weight>
524
- <italic>false</italic>
525
- <bold>true</bold>
526
- </font>
527
- </property>
528
- <property name="styleSheet">
529
- <string notr="true"/>
530
- </property>
531
- <property name="text">
532
- <string>Analyze New Genome</string>
533
- </property>
534
- <property name="scaledContents">
535
- <bool>false</bool>
536
- </property>
537
- </widget>
538
- </item>
539
  <item row="3" column="1" colspan="2">
540
  <widget class="QGroupBox" name="Step3">
541
  <property name="sizePolicy">
 
502
  </item>
503
  </layout>
504
  </item>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505
  <item row="3" column="1" colspan="2">
506
  <widget class="QGroupBox" name="Step3">
507
  <property name="sizePolicy">
src/ui/view_targets.ui CHANGED
@@ -6,8 +6,8 @@
6
  <rect>
7
  <x>0</x>
8
  <y>0</y>
9
- <width>1920</width>
10
- <height>1147</height>
11
  </rect>
12
  </property>
13
  <property name="font">
@@ -22,68 +22,6 @@
22
  <layout class="QGridLayout" name="gridLayout_2">
23
  <item row="0" column="0">
24
  <layout class="QGridLayout" name="gridLayout">
25
- <item row="0" column="1" rowspan="2">
26
- <widget class="QGroupBox" name="grpGeneViewer">
27
- <property name="sizePolicy">
28
- <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
29
- <horstretch>0</horstretch>
30
- <verstretch>0</verstretch>
31
- </sizepolicy>
32
- </property>
33
- <property name="title">
34
- <string>Gene Viewer</string>
35
- </property>
36
- <layout class="QGridLayout" name="gridLayout_6">
37
- <item row="2" column="1">
38
- <widget class="QLineEdit" name="ledStartLocation"/>
39
- </item>
40
- <item row="3" column="2">
41
- <widget class="QPushButton" name="pbtnResetLocation">
42
- <property name="text">
43
- <string>Reset Location</string>
44
- </property>
45
- </widget>
46
- </item>
47
- <item row="2" column="0">
48
- <widget class="QLabel" name="lblStartLocation">
49
- <property name="text">
50
- <string>Start:</string>
51
- </property>
52
- </widget>
53
- </item>
54
- <item row="3" column="1">
55
- <widget class="QLineEdit" name="ledStopLocation"/>
56
- </item>
57
- <item row="3" column="0">
58
- <widget class="QLabel" name="lblStopLocation">
59
- <property name="text">
60
- <string>Stop:</string>
61
- </property>
62
- </widget>
63
- </item>
64
- <item row="2" column="2">
65
- <widget class="QPushButton" name="pbtnChangeLocation">
66
- <property name="toolTip">
67
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This button changes the start and end location of the Gene Viewer sequence, based on what is entered in the Start and Stop boxes.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
68
- </property>
69
- <property name="text">
70
- <string>Change Location</string>
71
- </property>
72
- </widget>
73
- </item>
74
- <item row="5" column="0" colspan="5">
75
- <widget class="QTextEdit" name="txtedGeneViewer"/>
76
- </item>
77
- <item row="4" column="2">
78
- <widget class="QCheckBox" name="chkViewExonsOnly">
79
- <property name="text">
80
- <string>View Exons Only</string>
81
- </property>
82
- </widget>
83
- </item>
84
- </layout>
85
- </widget>
86
- </item>
87
  <item row="0" column="0">
88
  <widget class="QGroupBox" name="grpGuideViewer">
89
  <property name="sizePolicy">
@@ -217,6 +155,93 @@
217
  </layout>
218
  </widget>
219
  </item>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  </layout>
221
  </item>
222
  </layout>
 
6
  <rect>
7
  <x>0</x>
8
  <y>0</y>
9
+ <width>1512</width>
10
+ <height>916</height>
11
  </rect>
12
  </property>
13
  <property name="font">
 
22
  <layout class="QGridLayout" name="gridLayout_2">
23
  <item row="0" column="0">
24
  <layout class="QGridLayout" name="gridLayout">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  <item row="0" column="0">
26
  <widget class="QGroupBox" name="grpGuideViewer">
27
  <property name="sizePolicy">
 
155
  </layout>
156
  </widget>
157
  </item>
158
+ <item row="0" column="1">
159
+ <widget class="QGroupBox" name="grpGeneViewer">
160
+ <property name="sizePolicy">
161
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
162
+ <horstretch>0</horstretch>
163
+ <verstretch>0</verstretch>
164
+ </sizepolicy>
165
+ </property>
166
+ <property name="title">
167
+ <string>Gene Viewer</string>
168
+ </property>
169
+ <layout class="QGridLayout" name="gridLayout_6">
170
+ <item row="3" column="1">
171
+ <widget class="QLineEdit" name="ledStopLocation"/>
172
+ </item>
173
+ <item row="4" column="0" colspan="2">
174
+ <widget class="QLabel" name="lblSequenceLegend">
175
+ <property name="sizePolicy">
176
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
177
+ <horstretch>0</horstretch>
178
+ <verstretch>0</verstretch>
179
+ </sizepolicy>
180
+ </property>
181
+ <property name="layoutDirection">
182
+ <enum>Qt::LeftToRight</enum>
183
+ </property>
184
+ <property name="text">
185
+ <string>ACTG - Gene Sequence&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: rgb(100, 100, 100);&quot;&gt;actg&lt;/span&gt; - Padding Sequence</string>
186
+ </property>
187
+ </widget>
188
+ </item>
189
+ <item row="2" column="1">
190
+ <widget class="QLineEdit" name="ledStartLocation"/>
191
+ </item>
192
+ <item row="3" column="2">
193
+ <widget class="QPushButton" name="pbtnResetLocation">
194
+ <property name="text">
195
+ <string>Reset Location</string>
196
+ </property>
197
+ </widget>
198
+ </item>
199
+ <item row="3" column="0">
200
+ <widget class="QLabel" name="lblStopLocation">
201
+ <property name="text">
202
+ <string>Stop:</string>
203
+ </property>
204
+ </widget>
205
+ </item>
206
+ <item row="4" column="2">
207
+ <widget class="QCheckBox" name="chkViewExonsOnly">
208
+ <property name="autoFillBackground">
209
+ <bool>false</bool>
210
+ </property>
211
+ <property name="text">
212
+ <string>View Exons Only</string>
213
+ </property>
214
+ </widget>
215
+ </item>
216
+ <item row="5" column="0" colspan="5">
217
+ <widget class="QTextEdit" name="txtedGeneViewer"/>
218
+ </item>
219
+ <item row="2" column="0">
220
+ <widget class="QLabel" name="lblStartLocation">
221
+ <property name="text">
222
+ <string>Start:</string>
223
+ </property>
224
+ </widget>
225
+ </item>
226
+ <item row="2" column="2">
227
+ <widget class="QPushButton" name="pbtnChangeLocation">
228
+ <property name="sizePolicy">
229
+ <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
230
+ <horstretch>0</horstretch>
231
+ <verstretch>0</verstretch>
232
+ </sizepolicy>
233
+ </property>
234
+ <property name="toolTip">
235
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This button changes the start and end location of the Gene Viewer sequence, based on what is entered in the Start and Stop boxes.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
236
+ </property>
237
+ <property name="text">
238
+ <string>Change Location</string>
239
+ </property>
240
+ </widget>
241
+ </item>
242
+ </layout>
243
+ </widget>
244
+ </item>
245
  </layout>
246
  </item>
247
  </layout>
src/views/CloseableTabWidget.py CHANGED
@@ -6,6 +6,7 @@ import logging
6
 
7
  class CloseableTabWidget(QTabWidget):
8
  tab_closed = pyqtSignal(QWidget)
 
9
 
10
  def __init__(self, parent=None):
11
  super().__init__(parent)
@@ -19,27 +20,32 @@ class CloseableTabWidget(QTabWidget):
19
  """Close a tab at the given index"""
20
  self.logger.debug(f"Attempting to close tab at index {index}")
21
 
22
- if not (self.count() > 1 and index != 0):
23
- self.logger.debug("Tab closure conditions not met")
24
- return
25
-
 
 
26
  widget = self.widget(index)
27
  if not widget:
28
  self.logger.warning(f"No widget found at index {index}")
29
  return
30
-
31
- # Critical operations need try-catch
32
  try:
 
 
 
33
  tab_text = self.tabText(index)
34
 
35
  # Cleanup controller if exists
36
  controller = getattr(widget, 'controller', None)
37
- if controller and hasattr(controller, 'model') and hasattr(controller.model, 'cleanup'):
38
- controller.model.cleanup()
39
 
40
  # Remove from tracking and emit signal
41
- if tab_text in self._tabs:
42
- del self._tabs[tab_text]
 
43
 
44
  self.removeTab(index)
45
  self.tab_closed.emit(widget)
@@ -116,6 +122,8 @@ class CloseableTabWidget(QTabWidget):
116
  if 0 <= index < self.count():
117
  current_widget = self.widget(index)
118
  if current_widget and index != 0:
 
 
119
  self.closeTab(index)
120
  except Exception as e:
121
  self.logger.error(f"Error in safely_close_tab: {e}")
@@ -166,4 +174,28 @@ class CloseableTabWidget(QTabWidget):
166
  self._update_all_tabs()
167
 
168
  except Exception as e:
169
- self.logger.error(f"Error moving tab: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  class CloseableTabWidget(QTabWidget):
8
  tab_closed = pyqtSignal(QWidget)
9
+ tab_closing = pyqtSignal(int)
10
 
11
  def __init__(self, parent=None):
12
  super().__init__(parent)
 
20
  """Close a tab at the given index"""
21
  self.logger.debug(f"Attempting to close tab at index {index}")
22
 
23
+ # Skip normal closure conditions for forced closure
24
+ if not hasattr(self, '_force_close'):
25
+ if not (self.count() > 1 and index != 0):
26
+ self.logger.debug("Tab closure conditions not met")
27
+ return
28
+
29
  widget = self.widget(index)
30
  if not widget:
31
  self.logger.warning(f"No widget found at index {index}")
32
  return
33
+
 
34
  try:
35
+ # Emit signal before closing the tab
36
+ self.tab_closing.emit(index)
37
+
38
  tab_text = self.tabText(index)
39
 
40
  # Cleanup controller if exists
41
  controller = getattr(widget, 'controller', None)
42
+ # if controller and hasattr(controller, 'model') and hasattr(controller.model, 'cleanup'):
43
+ # controller.model.cleanup()
44
 
45
  # Remove from tracking and emit signal
46
+ tab_id = f"{tab_text}_{id(widget)}"
47
+ if tab_id in self._tabs:
48
+ del self._tabs[tab_id]
49
 
50
  self.removeTab(index)
51
  self.tab_closed.emit(widget)
 
122
  if 0 <= index < self.count():
123
  current_widget = self.widget(index)
124
  if current_widget and index != 0:
125
+ # Emit signal before closing
126
+ self.tab_closing.emit(index)
127
  self.closeTab(index)
128
  except Exception as e:
129
  self.logger.error(f"Error in safely_close_tab: {e}")
 
174
  self._update_all_tabs()
175
 
176
  except Exception as e:
177
+ self.logger.error(f"Error moving tab: {e}")
178
+
179
+ def removeTab(self, index):
180
+ """Override removeTab to handle cleanup"""
181
+ try:
182
+ widget = self.widget(index)
183
+ if widget:
184
+ # Get tab text before removal
185
+ tab_text = self.tabText(index)
186
+
187
+ # Remove from tracking dictionary
188
+ tab_id = f"{tab_text}_{id(widget)}"
189
+ if tab_id in self._tabs:
190
+ del self._tabs[tab_id]
191
+
192
+ # Remove the tab
193
+ super().removeTab(index)
194
+
195
+ # Cleanup the widget
196
+ widget.deleteLater()
197
+
198
+ self._update_all_tabs()
199
+
200
+ except Exception as e:
201
+ self.logger.error(f"Error removing tab: {e}")
src/views/FindTargetsView.py CHANGED
@@ -1,6 +1,6 @@
1
  from PyQt6 import QtWidgets
2
  from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
3
- QPushButton, QHBoxLayout, QLabel, QAbstractItemView)
4
  from PyQt6 import uic
5
  from PyQt6.QtCore import Qt, QTimer
6
  import time
@@ -17,10 +17,15 @@ class FindTargetsView(QtWidgets.QMainWindow):
17
 
18
  def _init_ui(self):
19
  uic.loadUi(self.global_settings.get_ui_dir_path() + '/find_targets.ui', self)
 
20
  self.results_table = self.findChild(QTableWidget, 'tblTargets')
21
 
 
 
 
22
  # Optimize table settings for large datasets
23
  self.results_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
 
24
  self.results_table.setShowGrid(False)
25
  self.results_table.setAlternatingRowColors(True)
26
 
@@ -134,23 +139,28 @@ class FindTargetsView(QtWidgets.QMainWindow):
134
  selected_rows = set(index.row() for index in self.results_table.selectedIndexes())
135
  selected_targets = []
136
 
 
 
 
 
137
  # Get the currently visible rows from the table
138
- visible_targets = []
139
- for row in range(self.results_table.rowCount()):
140
- if not self.results_table.isRowHidden(row):
 
 
 
 
 
141
  # Get data from visible row
142
  target_data = {
143
- 'feature_type': self.results_table.item(row, 0).text(),
144
- 'chromosome': self.results_table.item(row, 1).text(),
145
- 'feature_id': self.results_table.item(row, 2).text(),
146
- 'feature_name': self.results_table.item(row, 3).text(),
147
- 'feature_description': self.results_table.item(row, 4).text()
148
  }
149
- visible_targets.append((row, target_data))
150
-
151
- # Match selected rows with visible targets
152
- for row, target_data in visible_targets:
153
- if row in selected_rows:
154
  # Find corresponding full target data from _all_results
155
  for full_target in self._all_results:
156
  if (full_target['feature_id'] == target_data['feature_id'] and
@@ -158,6 +168,10 @@ class FindTargetsView(QtWidgets.QMainWindow):
158
  selected_targets.append(full_target)
159
  break
160
 
 
 
 
 
161
  self.logger.debug(f"Selected {len(selected_targets)} targets from filtered view")
162
  return selected_targets
163
 
@@ -173,8 +187,7 @@ class FindTargetsView(QtWidgets.QMainWindow):
173
  """Handle generate library button click"""
174
  try:
175
  selected_targets = self.get_selected_targets()
176
- self.global_settings.logger.debug(f"Selected {len(selected_targets)} targets for library generation")
177
-
178
  if not selected_targets:
179
  QtWidgets.QMessageBox.warning(
180
  self,
@@ -183,14 +196,15 @@ class FindTargetsView(QtWidgets.QMainWindow):
183
  )
184
  return
185
 
 
 
 
186
  # Create and show generate library window
187
- self.global_settings.logger.debug("Creating GenerateLibraryController")
188
  from controllers.GenerateLibraryController import GenerateLibraryController
189
  generate_library_controller = GenerateLibraryController(
190
  self.global_settings,
191
  selected_targets
192
  )
193
- self.global_settings.logger.debug("Showing generate library window")
194
  generate_library_controller.show()
195
 
196
  except Exception as e:
@@ -200,3 +214,15 @@ class FindTargetsView(QtWidgets.QMainWindow):
200
  "Error",
201
  f"An error occurred while opening the generate library window: {str(e)}"
202
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from PyQt6 import QtWidgets
2
  from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
3
+ QPushButton, QHBoxLayout, QLabel, QAbstractItemView, QCheckBox)
4
  from PyQt6 import uic
5
  from PyQt6.QtCore import Qt, QTimer
6
  import time
 
17
 
18
  def _init_ui(self):
19
  uic.loadUi(self.global_settings.get_ui_dir_path() + '/find_targets.ui', self)
20
+ self.checkbox_select_all = self.findChild(QCheckBox, 'chkSelectAll')
21
  self.results_table = self.findChild(QTableWidget, 'tblTargets')
22
 
23
+ # Connect select all checkbox signal
24
+ self.checkbox_select_all.stateChanged.connect(self._on_select_all_changed)
25
+
26
  # Optimize table settings for large datasets
27
  self.results_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
28
+ self.results_table.setSelectionMode(QTableWidget.SelectionMode.MultiSelection)
29
  self.results_table.setShowGrid(False)
30
  self.results_table.setAlternatingRowColors(True)
31
 
 
139
  selected_rows = set(index.row() for index in self.results_table.selectedIndexes())
140
  selected_targets = []
141
 
142
+ if not selected_rows:
143
+ self.logger.debug("No rows selected")
144
+ return []
145
+
146
  # Get the currently visible rows from the table
147
+ for row in selected_rows:
148
+ try:
149
+ # Check if all required cells have valid data
150
+ cells = [self.results_table.item(row, col) for col in range(5)]
151
+ if any(cell is None for cell in cells):
152
+ self.logger.warning(f"Row {row} has missing data, skipping")
153
+ continue
154
+
155
  # Get data from visible row
156
  target_data = {
157
+ 'feature_type': cells[0].text(),
158
+ 'chromosome': cells[1].text(),
159
+ 'feature_id': cells[2].text(),
160
+ 'feature_name': cells[3].text(),
161
+ 'feature_description': cells[4].text()
162
  }
163
+
 
 
 
 
164
  # Find corresponding full target data from _all_results
165
  for full_target in self._all_results:
166
  if (full_target['feature_id'] == target_data['feature_id'] and
 
168
  selected_targets.append(full_target)
169
  break
170
 
171
+ except Exception as row_error:
172
+ self.logger.warning(f"Error processing row {row}: {str(row_error)}")
173
+ continue
174
+
175
  self.logger.debug(f"Selected {len(selected_targets)} targets from filtered view")
176
  return selected_targets
177
 
 
187
  """Handle generate library button click"""
188
  try:
189
  selected_targets = self.get_selected_targets()
190
+
 
191
  if not selected_targets:
192
  QtWidgets.QMessageBox.warning(
193
  self,
 
196
  )
197
  return
198
 
199
+ # Store selected targets in global settings for persistence
200
+ self.global_settings._current_selected_targets = selected_targets
201
+
202
  # Create and show generate library window
 
203
  from controllers.GenerateLibraryController import GenerateLibraryController
204
  generate_library_controller = GenerateLibraryController(
205
  self.global_settings,
206
  selected_targets
207
  )
 
208
  generate_library_controller.show()
209
 
210
  except Exception as e:
 
214
  "Error",
215
  f"An error occurred while opening the generate library window: {str(e)}"
216
  )
217
+
218
+ def _on_select_all_changed(self, state):
219
+ """Handle select all checkbox state changes"""
220
+ try:
221
+ self.results_table.setUpdatesEnabled(False) # Disable updates for performance
222
+ if state == Qt.CheckState.Checked.value:
223
+ self.results_table.selectAll()
224
+ else:
225
+ self.results_table.clearSelection()
226
+ self.results_table.setUpdatesEnabled(True) # Re-enable updates
227
+ except Exception as e:
228
+ self.logger.error(f"Error in select all handler: {str(e)}")
src/views/GenerateLibraryView.py CHANGED
@@ -24,7 +24,6 @@ class GenerateLibraryView(QMainWindow):
24
  try:
25
  # Load UI file
26
  ui_file = os.path.join(self.global_settings.get_ui_dir_path(), 'generate_library.ui')
27
- self.logger.debug(f"Loading UI file from: {ui_file}")
28
  uic.loadUi(ui_file, self)
29
 
30
  # Set window properties
@@ -47,6 +46,9 @@ class GenerateLibraryView(QMainWindow):
47
  else:
48
  self.ledFilePath.setText(default_path + "/")
49
 
 
 
 
50
  # Center the window
51
  self._center_window()
52
 
@@ -139,11 +141,19 @@ class GenerateLibraryView(QMainWindow):
139
  )
140
  }
141
 
142
- if self.chkFindOffTargets.isChecked():
 
 
 
143
  try:
144
- settings['max_off_target_score'] = float(self.cmbMaximumOffTargetScore.text())
145
- except ValueError:
146
- raise ValueError("Invalid maximum off-target score")
 
 
 
 
 
147
 
148
  return settings
149
 
@@ -180,3 +190,12 @@ class GenerateLibraryView(QMainWindow):
180
  "Success",
181
  message
182
  )
 
 
 
 
 
 
 
 
 
 
24
  try:
25
  # Load UI file
26
  ui_file = os.path.join(self.global_settings.get_ui_dir_path(), 'generate_library.ui')
 
27
  uic.loadUi(ui_file, self)
28
 
29
  # Set window properties
 
46
  else:
47
  self.ledFilePath.setText(default_path + "/")
48
 
49
+ # Apply styles
50
+ self._set_styles()
51
+
52
  # Center the window
53
  self._center_window()
54
 
 
141
  )
142
  }
143
 
144
+ if settings['find_off_targets']:
145
+ max_off_target_score = self.cmbMaximumOffTargetScore.text().strip()
146
+ if not max_off_target_score:
147
+ raise ValueError("Please enter a maximum off-target score when Find Off Targets is enabled")
148
  try:
149
+ score = float(max_off_target_score)
150
+ if not 0 <= score <= 0.5:
151
+ raise ValueError("Maximum off-target score must be between 0 and 0.5 (inclusive)")
152
+ settings['max_off_target_score'] = score
153
+ except ValueError as e:
154
+ if str(e).startswith("Maximum"):
155
+ raise
156
+ raise ValueError("Invalid maximum off-target score - please enter a valid number")
157
 
158
  return settings
159
 
 
190
  "Success",
191
  message
192
  )
193
+
194
+ def _set_styles(self):
195
+ """Apply the global groupbox style"""
196
+ try:
197
+ style = self.global_settings.get_groupbox_style()
198
+ for groupbox in self.findChildren(QtWidgets.QGroupBox):
199
+ groupbox.setStyleSheet(style)
200
+ except Exception as e:
201
+ self.logger.error(f"Error setting styles: {str(e)}")
src/views/HomeWindowView.py CHANGED
@@ -14,9 +14,34 @@ class HomeWindowView(QWidget):
14
  try:
15
  uic.loadUi(os.path.join(self.global_settings.get_ui_dir_path(), "home_window.ui"), self)
16
  self._init_ui_elements()
 
17
  except Exception as e:
18
  self._handle_init_error(e)
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  def _init_ui_elements(self) -> None:
21
  # Create a main layout to hold everything
22
  main_layout = QVBoxLayout(self)
@@ -39,15 +64,15 @@ class HomeWindowView(QWidget):
39
  self._init_grpStep3()
40
 
41
  # Connect to database manager signals
42
- self.global_settings.db_manager.db_files_changed.connect(self._handle_db_files_changed)
43
- self.global_settings.db_manager.db_state_changed.connect(self._handle_db_state_changed)
44
 
45
  def _init_grpNavigationMenu(self) -> None:
46
  self.push_button_new_genome = self._find_widget("pbtnNewGenome", QPushButton)
47
  self.push_button_new_endonuclease = self._find_widget("pbtnNewEndonuclease", QPushButton)
48
  self.push_button_multitargeting_analysis = self._find_widget("pbtnMultitargetingAnalysis", QPushButton)
49
  self.push_button_population_analysis = self._find_widget("pbtnPopulationAnalysis", QPushButton)
50
- self.push_button_combine_files = self._find_widget("pbtnCombineFiles", QPushButton)
51
 
52
  def _init_grpStep1(self) -> None:
53
  self.combo_box_organism = self._find_widget("cmbOrganism", QComboBox)
@@ -90,17 +115,47 @@ class HomeWindowView(QWidget):
90
  show_error(self.global_settings, "Initialization Error", error_msg)
91
  raise
92
 
93
- def update_combo_box_endonuclease(self, endonuclease: list) -> None:
94
- self.combo_box_endonuclease.clear()
95
- self.combo_box_endonuclease.addItems(endonuclease)
 
 
 
 
 
 
 
 
 
 
96
 
97
- def update_combo_box_organism(self, organisms: list) -> None:
98
- self.combo_box_organism.clear()
99
- self.combo_box_organism.addItems(organisms)
 
 
 
 
 
 
 
 
 
 
100
 
101
- # def update_combo_box_annotation_files(self, annotation_files: list) -> None:
102
- # self.combo_box_local_annotation_files.clear()
103
- # self.combo_box_local_annotation_files.addItems(annotation_files)
 
 
 
 
 
 
 
 
 
 
104
 
105
  def get_find_targets_input(self) -> dict:
106
  current_annotation = self.combo_box_local_annotation_files.currentText()
@@ -125,32 +180,6 @@ class HomeWindowView(QWidget):
125
  def get_annotation_file(self) -> str:
126
  return self.combo_box_local_annotation_files.currentText()
127
 
128
- def update_combo_box_annotation_files(self, files):
129
- """Update local annotation files combo box, excluding .index files"""
130
- try:
131
- # Clear existing items
132
- self.combo_box_local_annotation_files.clear()
133
-
134
- # Filter out .index files and ensure files are valid
135
- filtered_files = [
136
- f for f in files
137
- if not f.endswith('.index') and f.strip()
138
- ]
139
-
140
- # Add filtered files to combo box
141
- if filtered_files:
142
- self.combo_box_local_annotation_files.addItems(filtered_files)
143
- # Set the first item as current
144
- self.combo_box_local_annotation_files.setCurrentIndex(0)
145
- # Emit the change signal to update the current annotation file
146
- self._on_annotation_file_changed(self.combo_box_local_annotation_files.currentText())
147
- self.logger.debug(f"Added {len(filtered_files)} local annotation files to combo box")
148
- else:
149
- self.logger.debug("No local annotation files found")
150
-
151
- except Exception as e:
152
- self.logger.error(f"Error updating local annotation files: {str(e)}")
153
-
154
  def _on_annotation_file_changed(self, new_file):
155
  """Handle annotation file changes"""
156
  try:
 
14
  try:
15
  uic.loadUi(os.path.join(self.global_settings.get_ui_dir_path(), "home_window.ui"), self)
16
  self._init_ui_elements()
17
+ self._set_styles()
18
  except Exception as e:
19
  self._handle_init_error(e)
20
 
21
+ def _set_styles(self) -> None:
22
+ """Set the styles for the groupboxes"""
23
+ groupbox_style = """
24
+ QGroupBox:title {
25
+ subcontrol-origin: margin;
26
+ left: 10px;
27
+ padding: 0 5px 0 5px;
28
+ }
29
+ QGroupBox#grpStep1, QGroupBox#grpStep2, QGroupBox#grpStep3 {
30
+ border: 2px solid rgb(111,181,110);
31
+ border-radius: 9px;
32
+ margin-top: 10px;
33
+ }
34
+ QGroupBox#grpNavigationMenu {
35
+ border: 2px dashed rgb(88,89,91);
36
+ border-radius: 9px;
37
+ margin-top: 10px;
38
+ }
39
+ """
40
+
41
+ # Find and style all groupboxes
42
+ for child in self.findChildren(QtWidgets.QGroupBox):
43
+ child.setStyleSheet(groupbox_style)
44
+
45
  def _init_ui_elements(self) -> None:
46
  # Create a main layout to hold everything
47
  main_layout = QVBoxLayout(self)
 
64
  self._init_grpStep3()
65
 
66
  # Connect to database manager signals
67
+ # self.global_settings.db_manager.db_files_changed.connect(self._handle_db_files_changed)
68
+ # self.global_settings.db_manager.db_state_changed.connect(self._handle_db_state_changed)
69
 
70
  def _init_grpNavigationMenu(self) -> None:
71
  self.push_button_new_genome = self._find_widget("pbtnNewGenome", QPushButton)
72
  self.push_button_new_endonuclease = self._find_widget("pbtnNewEndonuclease", QPushButton)
73
  self.push_button_multitargeting_analysis = self._find_widget("pbtnMultitargetingAnalysis", QPushButton)
74
  self.push_button_population_analysis = self._find_widget("pbtnPopulationAnalysis", QPushButton)
75
+ # self.push_button_combine_files = self._find_widget("pbtnCombineFiles", QPushButton)
76
 
77
  def _init_grpStep1(self) -> None:
78
  self.combo_box_organism = self._find_widget("cmbOrganism", QComboBox)
 
115
  show_error(self.global_settings, "Initialization Error", error_msg)
116
  raise
117
 
118
+ def update_combo_box_endonuclease(self, endonucleases):
119
+ """Update the endonuclease combo box"""
120
+ try:
121
+ current_text = self.combo_box_endonuclease.currentText()
122
+ self.combo_box_endonuclease.clear()
123
+ self.combo_box_endonuclease.addItems(endonucleases)
124
+
125
+ # Try to restore previous selection if it still exists
126
+ index = self.combo_box_endonuclease.findText(current_text)
127
+ if index >= 0:
128
+ self.combo_box_endonuclease.setCurrentIndex(index)
129
+ except Exception as e:
130
+ self.logger.error(f"Error updating endonuclease combo box: {str(e)}")
131
 
132
+ def update_combo_box_organism(self, organisms):
133
+ """Update the organism combo box"""
134
+ try:
135
+ current_text = self.combo_box_organism.currentText()
136
+ self.combo_box_organism.clear()
137
+ self.combo_box_organism.addItems(organisms)
138
+
139
+ # Try to restore previous selection if it still exists
140
+ index = self.combo_box_organism.findText(current_text)
141
+ if index >= 0:
142
+ self.combo_box_organism.setCurrentIndex(index)
143
+ except Exception as e:
144
+ self.logger.error(f"Error updating organism combo box: {str(e)}")
145
 
146
+ def update_combo_box_annotation_files(self, files):
147
+ """Update the annotation files combo box"""
148
+ try:
149
+ current_text = self.combo_box_local_annotation_files.currentText()
150
+ self.combo_box_local_annotation_files.clear()
151
+ self.combo_box_local_annotation_files.addItems(files)
152
+
153
+ # Try to restore previous selection if it still exists
154
+ index = self.combo_box_local_annotation_files.findText(current_text)
155
+ if index >= 0:
156
+ self.combo_box_local_annotation_files.setCurrentIndex(index)
157
+ except Exception as e:
158
+ self.logger.error(f"Error updating annotation files combo box: {str(e)}")
159
 
160
  def get_find_targets_input(self) -> dict:
161
  current_annotation = self.combo_box_local_annotation_files.currentText()
 
180
  def get_annotation_file(self) -> str:
181
  return self.combo_box_local_annotation_files.currentText()
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  def _on_annotation_file_changed(self, new_file):
184
  """Handle annotation file changes"""
185
  try:
src/views/LoadingDialog.py CHANGED
@@ -25,36 +25,18 @@ class LoadingDialog(QDialog):
25
  layout.addWidget(self.progress_bar)
26
 
27
  self.setLayout(layout)
28
-
29
- # Center on main window
 
 
30
  self.center_on_parent()
31
 
32
  def center_on_parent(self):
33
- """Center the dialog on the main window or parent"""
34
- parent = self.parent()
35
- if parent:
36
- # Get the main window from global settings if available
37
- main_window = None
38
- if hasattr(parent, 'global_settings'):
39
- main_window = parent.global_settings.main_window
40
- elif hasattr(parent, 'settings'):
41
- main_window = parent.settings.main_window
42
-
43
- # Get geometry of the window to center on
44
- if main_window and main_window.view:
45
- geometry = main_window.view.geometry()
46
- else:
47
- geometry = parent.geometry()
48
-
49
- # Calculate center position
50
- x = geometry.x() + (geometry.width() - self.width()) // 2
51
- y = geometry.y() + (geometry.height() - self.height()) // 2
52
-
53
- # Ensure dialog stays within screen bounds
54
- screen = QApplication.primaryScreen().geometry()
55
- x = max(screen.left(), min(x, screen.right() - self.width()))
56
- y = max(screen.top(), min(y, screen.bottom() - self.height()))
57
-
58
  self.move(x, y)
59
 
60
  def set_message(self, message, progress=None):
@@ -62,8 +44,6 @@ class LoadingDialog(QDialog):
62
  if progress is not None:
63
  self.progress_bar.setValue(progress)
64
  self.label.setText(message)
65
-
66
- # Recenter after updating message
67
  self.center_on_parent()
68
 
69
  def set_progress(self, value):
@@ -75,9 +55,4 @@ class LoadingDialog(QDialog):
75
  """Set indeterminate progress"""
76
  self.progress_bar.setRange(0, 0)
77
  self.label.setText("Loading...")
78
-
79
- def showEvent(self, event):
80
- """Override show event to ensure dialog is centered when shown"""
81
- super().showEvent(event)
82
- self.center_on_parent()
83
 
 
25
  layout.addWidget(self.progress_bar)
26
 
27
  self.setLayout(layout)
28
+
29
+ def showEvent(self, event):
30
+ """Override show event to ensure dialog is centered when shown"""
31
+ super().showEvent(event)
32
  self.center_on_parent()
33
 
34
  def center_on_parent(self):
35
+ """Center the dialog on the parent window"""
36
+ if self.parent():
37
+ parent_geometry = self.parent().geometry()
38
+ x = parent_geometry.x() + (parent_geometry.width() - self.width()) // 2
39
+ y = parent_geometry.y() + (parent_geometry.height() - self.height()) // 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  self.move(x, y)
41
 
42
  def set_message(self, message, progress=None):
 
44
  if progress is not None:
45
  self.progress_bar.setValue(progress)
46
  self.label.setText(message)
 
 
47
  self.center_on_parent()
48
 
49
  def set_progress(self, value):
 
55
  """Set indeterminate progress"""
56
  self.progress_bar.setRange(0, 0)
57
  self.label.setText("Loading...")
 
 
 
 
 
58
 
src/views/MultitargetingWindowView.py CHANGED
@@ -25,9 +25,19 @@ class MultitargetingWindowView(QtWidgets.QMainWindow):
25
  try:
26
  uic.loadUi(self.settings.get_ui_dir_path() + '/multitargeting_window.ui', self)
27
  self._init_ui_components()
 
28
  except Exception as e:
29
  show_error(self.settings, "Error initializing MultitargetingWindowView", str(e))
30
 
 
 
 
 
 
 
 
 
 
31
  def _init_ui_components(self):
32
  self._init_grpSelectOrganism()
33
  self._init_grpSeedAnalysis()
 
25
  try:
26
  uic.loadUi(self.settings.get_ui_dir_path() + '/multitargeting_window.ui', self)
27
  self._init_ui_components()
28
+ self._set_styles()
29
  except Exception as e:
30
  show_error(self.settings, "Error initializing MultitargetingWindowView", str(e))
31
 
32
+ def _set_styles(self):
33
+ """Apply the global groupbox style"""
34
+ try:
35
+ style = self.settings.get_groupbox_style()
36
+ for groupbox in self.findChildren(QtWidgets.QGroupBox):
37
+ groupbox.setStyleSheet(style)
38
+ except Exception as e:
39
+ self.logger.error(f"Error setting styles: {str(e)}")
40
+
41
  def _init_ui_components(self):
42
  self._init_grpSelectOrganism()
43
  self._init_grpSeedAnalysis()
src/views/NCBIWindowView.py CHANGED
@@ -2,6 +2,7 @@ from PyQt6 import QtWidgets, QtGui, QtCore, uic
2
  from PyQt6.QtWidgets import QWidget, QVBoxLayout
3
  import os
4
  from typing import Optional
 
5
 
6
  class NCBIWindowView(QtWidgets.QMainWindow):
7
  initialization_complete = QtCore.pyqtSignal() # New signal
@@ -18,27 +19,53 @@ class NCBIWindowView(QtWidgets.QMainWindow):
18
  def _setup_basic_ui(self):
19
  """Initial minimal setup to show the window quickly"""
20
  try:
 
 
 
 
21
  uic.loadUi(os.path.join(self.settings.get_ui_dir_path(), "ncbi.ui"), self)
 
 
 
 
22
 
23
  QtCore.QTimer.singleShot(100, self._complete_initialization)
24
 
 
25
  except Exception as e:
26
  self.logger.error(f"Error in basic UI setup: {str(e)}")
27
  raise
28
 
 
 
 
 
 
 
 
 
 
29
  def _complete_initialization(self):
30
  """Complete the full initialization of UI components"""
31
  try:
32
  if self._is_initialized:
33
  return
34
 
 
 
 
35
  # Initialize all UI components
 
36
  self._init_ui_components()
 
37
 
38
  self._is_initialized = True
39
 
40
  # Emit signal after everything is initialized
41
  self.initialization_complete.emit()
 
 
 
42
 
43
  except Exception as e:
44
  self.logger.error(f"Error in complete initialization: {str(e)}")
@@ -47,9 +74,20 @@ class NCBIWindowView(QtWidgets.QMainWindow):
47
  def _init_ui_components(self) -> None:
48
  """Initialize all UI components at once instead of using timers"""
49
  try:
 
 
 
50
  self._init_grpStep1()
 
 
 
51
  self._init_grpStep2()
 
 
 
52
  self._init_grpStep3()
 
 
53
  except Exception as e:
54
  self.logger.error(f"Error in _init_ui_components: {str(e)}")
55
  raise
 
2
  from PyQt6.QtWidgets import QWidget, QVBoxLayout
3
  import os
4
  from typing import Optional
5
+ import time
6
 
7
  class NCBIWindowView(QtWidgets.QMainWindow):
8
  initialization_complete = QtCore.pyqtSignal() # New signal
 
19
  def _setup_basic_ui(self):
20
  """Initial minimal setup to show the window quickly"""
21
  try:
22
+ start_time = time.time()
23
+ self.logger.debug("Starting basic UI setup")
24
+
25
+ ui_load_start = time.time()
26
  uic.loadUi(os.path.join(self.settings.get_ui_dir_path(), "ncbi.ui"), self)
27
+ self.logger.debug(f"Loading UI file took: {time.time() - ui_load_start:.2f} seconds")
28
+
29
+ # Apply styles immediately for better visual experience
30
+ self._set_styles()
31
 
32
  QtCore.QTimer.singleShot(100, self._complete_initialization)
33
 
34
+ self.logger.debug(f"Basic UI setup completed in: {time.time() - start_time:.2f} seconds")
35
  except Exception as e:
36
  self.logger.error(f"Error in basic UI setup: {str(e)}")
37
  raise
38
 
39
+ def _set_styles(self):
40
+ """Apply the global groupbox style"""
41
+ try:
42
+ style = self.settings.get_groupbox_style()
43
+ for groupbox in self.findChildren(QtWidgets.QGroupBox):
44
+ groupbox.setStyleSheet(style)
45
+ except Exception as e:
46
+ self.logger.error(f"Error setting styles: {str(e)}")
47
+
48
  def _complete_initialization(self):
49
  """Complete the full initialization of UI components"""
50
  try:
51
  if self._is_initialized:
52
  return
53
 
54
+ start_time = time.time()
55
+ self.logger.debug("Starting complete initialization")
56
+
57
  # Initialize all UI components
58
+ components_start = time.time()
59
  self._init_ui_components()
60
+ self.logger.debug(f"UI components initialization took: {time.time() - components_start:.2f} seconds")
61
 
62
  self._is_initialized = True
63
 
64
  # Emit signal after everything is initialized
65
  self.initialization_complete.emit()
66
+ self.logger.debug("Emitted initialization complete signal")
67
+
68
+ self.logger.debug(f"Complete initialization finished in: {time.time() - start_time:.2f} seconds")
69
 
70
  except Exception as e:
71
  self.logger.error(f"Error in complete initialization: {str(e)}")
 
74
  def _init_ui_components(self) -> None:
75
  """Initialize all UI components at once instead of using timers"""
76
  try:
77
+ self.logger.debug("Starting UI components initialization")
78
+
79
+ step1_start = time.time()
80
  self._init_grpStep1()
81
+ self.logger.debug(f"Step 1 initialization took: {time.time() - step1_start:.2f} seconds")
82
+
83
+ step2_start = time.time()
84
  self._init_grpStep2()
85
+ self.logger.debug(f"Step 2 initialization took: {time.time() - step2_start:.2f} seconds")
86
+
87
+ step3_start = time.time()
88
  self._init_grpStep3()
89
+ self.logger.debug(f"Step 3 initialization took: {time.time() - step3_start:.2f} seconds")
90
+
91
  except Exception as e:
92
  self.logger.error(f"Error in _init_ui_components: {str(e)}")
93
  raise
src/views/NewEndonucleaseView.py CHANGED
@@ -99,16 +99,6 @@ class NewEndonucleaseView(QtWidgets.QMainWindow):
99
  image: none;
100
  border: none;
101
  }}
102
- QGroupBox {{
103
- border: 1px solid {theme['button_border_color']};
104
- margin-top: 1em;
105
- padding-top: 0.5em;
106
- }}
107
- QGroupBox::title {{
108
- subcontrol-origin: margin;
109
- left: 10px;
110
- padding: 0 3px 0 3px;
111
- }}
112
  QRadioButton {{
113
  color: {theme['fg_color']};
114
  }}
@@ -132,19 +122,32 @@ class NewEndonucleaseView(QtWidgets.QMainWindow):
132
  """)
133
 
134
  def showEvent(self, event):
135
- """Override showEvent to apply theme when window is shown"""
136
  super().showEvent(event)
137
- self.apply_theme()
138
 
139
  def init_ui(self):
140
  try:
141
  uic.loadUi(os.path.join(self.settings.get_ui_dir_path(), 'new_endonuclease_window.ui'), self)
142
-
143
  self._init_ui_components()
 
144
  self.disable_form_elements()
145
  except Exception as e:
146
  show_error(self.settings, "Error initializing NewEndonucleaseView", str(e))
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  def _init_ui_components(self):
149
  self.combo_box_select_endonuclease = self._find_widget('cmbSelectEndonuclease', QtWidgets.QComboBox)
150
 
 
99
  image: none;
100
  border: none;
101
  }}
 
 
 
 
 
 
 
 
 
 
102
  QRadioButton {{
103
  color: {theme['fg_color']};
104
  }}
 
122
  """)
123
 
124
  def showEvent(self, event):
125
+ """Override showEvent to apply theme and styles when window is shown"""
126
  super().showEvent(event)
127
+ self._set_styles()
128
 
129
  def init_ui(self):
130
  try:
131
  uic.loadUi(os.path.join(self.settings.get_ui_dir_path(), 'new_endonuclease_window.ui'), self)
 
132
  self._init_ui_components()
133
+ self._set_styles() # Add style initialization
134
  self.disable_form_elements()
135
  except Exception as e:
136
  show_error(self.settings, "Error initializing NewEndonucleaseView", str(e))
137
 
138
+ def _set_styles(self):
139
+ """Apply the global groupbox style and theme"""
140
+ try:
141
+ # Apply global groupbox style
142
+ style = self.settings.get_groupbox_style()
143
+ for groupbox in self.findChildren(QtWidgets.QGroupBox):
144
+ groupbox.setStyleSheet(style)
145
+
146
+ # Apply theme
147
+ self.apply_theme()
148
+ except Exception as e:
149
+ self.logger.error(f"Error setting styles: {str(e)}")
150
+
151
  def _init_ui_components(self):
152
  self.combo_box_select_endonuclease = self._find_widget('cmbSelectEndonuclease', QtWidgets.QComboBox)
153
 
src/views/NewGenomeWindowView.py CHANGED
@@ -18,23 +18,17 @@ class NewGenomeWindowView(QtWidgets.QMainWindow):
18
 
19
  def _init_ui(self):
20
  uic.loadUi(os.path.join(self.global_settings.get_ui_dir_path(), 'new_genome_window.ui'), self)
21
- # self.set_styles()
22
-
23
  self._init_ui_components()
24
-
25
- def set_styles(self):
26
- groupbox_style = """
27
- QGroupBox:title{subcontrol-origin: margin;
28
- left: 10px;
29
- padding: 0 5px 0 5px;}
30
- QGroupBox#Step1{border: 2px solid rgb(111,181,110);
31
- border-radius: 9px;
32
- font: bold 14pt 'Arial';
33
- margin-top: 10px;}"""
34
-
35
- self.Step1.setStyleSheet(groupbox_style)
36
- self.Step2.setStyleSheet(groupbox_style.replace("Step1","Step2"))
37
- self.Step3.setStyleSheet(groupbox_style.replace("Step1","Step3"))
38
 
39
  def _init_ui_components(self):
40
  self._init_grpStep1()
 
18
 
19
  def _init_ui(self):
20
  uic.loadUi(os.path.join(self.global_settings.get_ui_dir_path(), 'new_genome_window.ui'), self)
 
 
21
  self._init_ui_components()
22
+ self._set_styles()
23
+
24
+ def _set_styles(self):
25
+ """Apply the global groupbox style"""
26
+ try:
27
+ style = self.global_settings.get_groupbox_style()
28
+ for groupbox in self.findChildren(QtWidgets.QGroupBox):
29
+ groupbox.setStyleSheet(style)
30
+ except Exception as e:
31
+ self.logger.error(f"Error setting styles: {str(e)}")
 
 
 
 
32
 
33
  def _init_ui_components(self):
34
  self._init_grpStep1()
src/views/PopulationAnalysisWindowView.py CHANGED
@@ -24,6 +24,7 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
24
  try:
25
  uic.loadUi(self.settings.get_ui_dir_path() + '/population_analysis.ui', self)
26
  self._init_ui_components()
 
27
  except Exception as e:
28
  show_error(self.settings, "Error initializing PopulationAnalysisWindowView", str(e))
29
 
@@ -35,8 +36,6 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
35
 
36
  def _init_grpSelectOrganisms(self):
37
  try:
38
- self.logger.debug("Starting _init_grpSelectOrganisms")
39
-
40
  self.combo_box_endonuclease = self._find_widget('cmbEndonuclease', QtWidgets.QComboBox)
41
  self.table_organism = self._find_widget('tblOrganism', QtWidgets.QTableWidget)
42
  self.push_button_analyze_organism = self._find_widget('pbtnAnalyzeOrganism', QtWidgets.QPushButton)
@@ -46,9 +45,6 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
46
  self.tab_shared_seed_heatmap = self._find_widget('tabSharedSeedHeatmap', QtWidgets.QWidget)
47
  self.heatmap_seed = self._find_widget('heatmapSeed', QtWidgets.QWidget)
48
 
49
- self.logger.debug(f"Tab widget found: {self.tab_widget_shared_seeds_heatmap is not None}")
50
- self.logger.debug(f"Heatmap widget found: {self.heatmap_seed is not None}")
51
-
52
  # Create layout for heatmap
53
  self.colormap_layout = QtWidgets.QVBoxLayout(self.heatmap_seed)
54
  self.colormap_layout.setContentsMargins(0, 0, 0, 0)
@@ -58,7 +54,6 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
58
  self.colormap_layout.addWidget(self.colormap_canvas)
59
 
60
  # Set up the organism table
61
- self.logger.debug("Setting up organism table")
62
  self.table_organism.setColumnCount(1)
63
  self.table_organism.setShowGrid(False)
64
  self.table_organism.setHorizontalHeaderLabels(["Organism"])
@@ -68,8 +63,6 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
68
  self.table_organism.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
69
  self.table_organism.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
70
 
71
- self.logger.debug("Completed _init_grpSelectOrganisms")
72
-
73
  except Exception as e:
74
  self.logger.error(f"Error in _init_grpSelectOrganisms: {str(e)}")
75
  self.logger.exception("Full traceback:")
@@ -138,7 +131,6 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
138
  def update_shared_seeds_table(self, seed_data):
139
  self.table_seed.setRowCount(len(seed_data))
140
  for row, data in enumerate(seed_data):
141
- print(data)
142
  for col, value in enumerate(data):
143
  item = QtWidgets.QTableWidgetItem(str(value))
144
  item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
@@ -148,7 +140,6 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
148
  def update_loc_finder_table(self, loc_data):
149
  self.table_locations.setRowCount(len(loc_data))
150
  for row, data in enumerate(loc_data):
151
- print(data)
152
  for col, key in enumerate(['seed', 'sequence', 'organism', 'chromosome', 'location']):
153
  item = QtWidgets.QTableWidgetItem(str(data[key]))
154
  item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
@@ -253,35 +244,22 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
253
  self.table_seed.setRowCount(0)
254
 
255
  def clear_loc_finder_table(self):
256
- self.loc_finder_table.setRowCount(0)
257
 
258
  def update_endo_dropdown(self, endos):
259
  """Update the endonuclease dropdown with the provided options"""
260
  try:
261
- self.logger.info("Starting update_endo_dropdown")
262
- self.logger.debug(f"Received endos: {endos}")
263
-
264
- print(self.combo_box_endonuclease)
265
-
266
- # if not self.combo_box_endonuclease:
267
- # self.logger.error("combo_box_endonuclease is None")
268
- # return
269
-
270
  self.combo_box_endonuclease.clear()
271
  self.combo_box_endonuclease.addItems(endos)
272
-
273
- self.logger.info(f"Updated endonuclease dropdown with {len(endos)} options")
274
- self.logger.debug(f"Current items in dropdown: {[self.combo_box_endonuclease.itemText(i) for i in range(self.combo_box_endonuclease.count())]}")
275
  except Exception as e:
276
  self.logger.error(f"Error updating endonuclease dropdown: {str(e)}")
277
- self.logger.exception("Full traceback:")
278
  show_error(self.settings, "Error updating endonuclease dropdown", str(e))
279
 
280
  def sort_table2(self, column):
281
  self.table_seed.sortItems(column)
282
 
283
  def sort_loc_finder_table(self, column):
284
- self.loc_finder_table.sortItems(column)
285
 
286
  def _on_theme_changed(self, theme):
287
  """Handle theme changes by updating the plot"""
@@ -307,6 +285,15 @@ class PopulationAnalysisWindowView(QtWidgets.QMainWindow):
307
  except Exception as e:
308
  self.logger.error(f"Error updating plot theme: {str(e)}")
309
 
 
 
 
 
 
 
 
 
 
310
  class MplCanvas(FigureCanvasQTAgg):
311
  def __init__(self, parent=None, width=8, height=6, dpi=100):
312
  self.fig = Figure(figsize=(width, height), dpi=dpi)
 
24
  try:
25
  uic.loadUi(self.settings.get_ui_dir_path() + '/population_analysis.ui', self)
26
  self._init_ui_components()
27
+ self._set_styles()
28
  except Exception as e:
29
  show_error(self.settings, "Error initializing PopulationAnalysisWindowView", str(e))
30
 
 
36
 
37
  def _init_grpSelectOrganisms(self):
38
  try:
 
 
39
  self.combo_box_endonuclease = self._find_widget('cmbEndonuclease', QtWidgets.QComboBox)
40
  self.table_organism = self._find_widget('tblOrganism', QtWidgets.QTableWidget)
41
  self.push_button_analyze_organism = self._find_widget('pbtnAnalyzeOrganism', QtWidgets.QPushButton)
 
45
  self.tab_shared_seed_heatmap = self._find_widget('tabSharedSeedHeatmap', QtWidgets.QWidget)
46
  self.heatmap_seed = self._find_widget('heatmapSeed', QtWidgets.QWidget)
47
 
 
 
 
48
  # Create layout for heatmap
49
  self.colormap_layout = QtWidgets.QVBoxLayout(self.heatmap_seed)
50
  self.colormap_layout.setContentsMargins(0, 0, 0, 0)
 
54
  self.colormap_layout.addWidget(self.colormap_canvas)
55
 
56
  # Set up the organism table
 
57
  self.table_organism.setColumnCount(1)
58
  self.table_organism.setShowGrid(False)
59
  self.table_organism.setHorizontalHeaderLabels(["Organism"])
 
63
  self.table_organism.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
64
  self.table_organism.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
65
 
 
 
66
  except Exception as e:
67
  self.logger.error(f"Error in _init_grpSelectOrganisms: {str(e)}")
68
  self.logger.exception("Full traceback:")
 
131
  def update_shared_seeds_table(self, seed_data):
132
  self.table_seed.setRowCount(len(seed_data))
133
  for row, data in enumerate(seed_data):
 
134
  for col, value in enumerate(data):
135
  item = QtWidgets.QTableWidgetItem(str(value))
136
  item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
 
140
  def update_loc_finder_table(self, loc_data):
141
  self.table_locations.setRowCount(len(loc_data))
142
  for row, data in enumerate(loc_data):
 
143
  for col, key in enumerate(['seed', 'sequence', 'organism', 'chromosome', 'location']):
144
  item = QtWidgets.QTableWidgetItem(str(data[key]))
145
  item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
 
244
  self.table_seed.setRowCount(0)
245
 
246
  def clear_loc_finder_table(self):
247
+ self.table_locations.setRowCount(0)
248
 
249
  def update_endo_dropdown(self, endos):
250
  """Update the endonuclease dropdown with the provided options"""
251
  try:
 
 
 
 
 
 
 
 
 
252
  self.combo_box_endonuclease.clear()
253
  self.combo_box_endonuclease.addItems(endos)
 
 
 
254
  except Exception as e:
255
  self.logger.error(f"Error updating endonuclease dropdown: {str(e)}")
 
256
  show_error(self.settings, "Error updating endonuclease dropdown", str(e))
257
 
258
  def sort_table2(self, column):
259
  self.table_seed.sortItems(column)
260
 
261
  def sort_loc_finder_table(self, column):
262
+ self.table_locations.sortItems(column)
263
 
264
  def _on_theme_changed(self, theme):
265
  """Handle theme changes by updating the plot"""
 
285
  except Exception as e:
286
  self.logger.error(f"Error updating plot theme: {str(e)}")
287
 
288
+ def _set_styles(self):
289
+ """Apply the global groupbox style"""
290
+ try:
291
+ style = self.settings.get_groupbox_style()
292
+ for groupbox in self.findChildren(QtWidgets.QGroupBox):
293
+ groupbox.setStyleSheet(style)
294
+ except Exception as e:
295
+ self.logger.error(f"Error setting styles: {str(e)}")
296
+
297
  class MplCanvas(FigureCanvasQTAgg):
298
  def __init__(self, parent=None, width=8, height=6, dpi=100):
299
  self.fig = Figure(figsize=(width, height), dpi=dpi)
src/views/StartupWindowView.py CHANGED
@@ -75,9 +75,10 @@ class StartupWindowView(QtWidgets.QMainWindow):
75
  if is_valid:
76
  self.label_db_status.hide()
77
  self.push_button_go_to_home_or_new_genome.setText("Go to Home")
 
78
  else:
79
  self.label_db_status.setText(message)
80
  self.label_db_status.show()
81
  self.label_db_status.setStyleSheet("color: red;")
82
  self.push_button_go_to_home_or_new_genome.setText("Analyze a New Genome")
83
- self.push_button_go_to_home_or_new_genome.setEnabled(True)
 
75
  if is_valid:
76
  self.label_db_status.hide()
77
  self.push_button_go_to_home_or_new_genome.setText("Go to Home")
78
+ self.push_button_go_to_home_or_new_genome.setEnabled(True)
79
  else:
80
  self.label_db_status.setText(message)
81
  self.label_db_status.show()
82
  self.label_db_status.setStyleSheet("color: red;")
83
  self.push_button_go_to_home_or_new_genome.setText("Analyze a New Genome")
84
+ self.push_button_go_to_home_or_new_genome.setEnabled(True)
src/views/ViewTargetsView.py CHANGED
@@ -1,11 +1,12 @@
1
  from typing import Optional
2
  from PyQt6 import QtWidgets, uic
3
- from PyQt6.QtWidgets import QTableWidgetItem, QAbstractItemView
4
- from PyQt6.QtGui import QTextDocument
5
- from PyQt6.QtCore import Qt, pyqtSignal
6
  from utils.ui import show_error
7
  import traceback
8
- from views.DNAFeatureViewer import DNAFeatureViewer
 
9
 
10
  class ViewTargetsView(QtWidgets.QMainWindow):
11
  # Define the signal
@@ -21,10 +22,21 @@ class ViewTargetsView(QtWidgets.QMainWindow):
21
  def init_ui(self):
22
  try:
23
  uic.loadUi(self.settings.get_ui_dir_path() + '/view_targets.ui', self)
 
24
  self._init_ui_components()
 
25
  except Exception as e:
26
  show_error(self.settings, "Error initializing ViewTargetsView", str(e))
27
 
 
 
 
 
 
 
 
 
 
28
  def _init_ui_components(self):
29
  self._init_grpGuideViewer()
30
  self._init_grpGuideAnalysis()
@@ -75,6 +87,7 @@ class ViewTargetsView(QtWidgets.QMainWindow):
75
  self.push_button_cotargeting = self._find_widget('pbtnCoTargeting', QtWidgets.QPushButton)
76
 
77
  def _init_grpGeneViewer(self):
 
78
  self.push_button_highlight_guides = self._find_widget('pbtnHighlightGuides', QtWidgets.QPushButton)
79
  self.push_button_clear_guides = self._find_widget('pbtnClearGuides', QtWidgets.QPushButton)
80
  self.line_edit_start_location = self._find_widget('ledStartLocation', QtWidgets.QLineEdit)
@@ -82,31 +95,47 @@ class ViewTargetsView(QtWidgets.QMainWindow):
82
  self.push_button_change_location = self._find_widget('pbtnChangeLocation', QtWidgets.QPushButton)
83
  self.text_edit_gene_viewer = self._find_widget('txtedGeneViewer', QtWidgets.QTextEdit)
84
  self.push_button_reset_location = self._find_widget('pbtnResetLocation', QtWidgets.QPushButton)
 
85
  self.check_box_view_exons_only = self._find_widget('chkViewExonsOnly', QtWidgets.QCheckBox)
86
 
87
- self.text_edit_gene_viewer.setReadOnly(True)
 
88
 
89
- # Create DNA feature viewer
90
- self.dna_feature_viewer = DNAFeatureViewer()
91
-
92
- # Get the layout of the gene viewer group
93
- gene_viewer_group = self.findChild(QtWidgets.QGroupBox, 'grpGeneViewer')
94
- gene_viewer_layout = gene_viewer_group.layout()
95
-
96
- # Find the row index of the text editor
97
- text_editor_row = -1
98
- for i in range(gene_viewer_layout.rowCount()):
99
- item = gene_viewer_layout.itemAtPosition(i, 0)
100
- if item and item.widget() == self.text_edit_gene_viewer:
101
- text_editor_row = i
102
- break
103
-
104
- if text_editor_row != -1:
105
- # Insert DNA feature viewer above the text editor
106
- gene_viewer_layout.addWidget(self.dna_feature_viewer, text_editor_row, 0, 1, -1)
107
-
108
- # Connect signals
109
- self.dna_feature_viewer.sequence_selected.connect(self._on_sequence_selected)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
112
  widget = self.findChild(widget_type, name)
@@ -114,10 +143,9 @@ class ViewTargetsView(QtWidgets.QMainWindow):
114
  self.logger.warning(f"Widget '{name}' not found in UI file.")
115
  return widget
116
 
117
- def display_guides_in_table(self, guides):
118
  try:
119
  self._all_guides = guides
120
-
121
  selected_text = self.combo_box_gene.currentText()
122
 
123
  # First filter by position/feature
@@ -138,6 +166,7 @@ class ViewTargetsView(QtWidgets.QMainWindow):
138
  filtered_guides.append(guide)
139
  else:
140
  filtered_guides = self._all_guides
 
141
 
142
  # Apply additional filters
143
  final_guides = []
@@ -157,13 +186,13 @@ class ViewTargetsView(QtWidgets.QMainWindow):
157
 
158
  # Update table with new guides
159
  total_rows = len(final_guides)
160
- self.logger.debug(f"Processing {total_rows} rows for display after filtering")
161
 
162
- # Completely freeze UI
163
- self.setUpdatesEnabled(False)
164
- self.table_guides.setUpdatesEnabled(False)
165
- self.table_guides.setSortingEnabled(False)
166
- self.table_guides.setVisible(False)
 
167
 
168
  try:
169
  # Clear and resize table
@@ -237,11 +266,12 @@ class ViewTargetsView(QtWidgets.QMainWindow):
237
  guide_viewer_group.setMinimumWidth(essential_columns_width + 50) # Add some padding for scrollbar
238
 
239
  finally:
240
- # Re-enable UI
241
- self.table_guides.setVisible(True)
242
- self.table_guides.setUpdatesEnabled(True)
243
- self.setUpdatesEnabled(True)
244
- self.table_guides.setSortingEnabled(True)
 
245
 
246
  except Exception as e:
247
  self.logger.error(f"Error in display_guides: {str(e)}")
@@ -390,9 +420,6 @@ class ViewTargetsView(QtWidgets.QMainWindow):
390
  # Clear existing items efficiently
391
  self.combo_box_gene.clear()
392
 
393
- # Debug logging
394
- self.logger.debug(f"Received {len(genes)} genes")
395
-
396
  # Use a set to ensure uniqueness
397
  unique_genes = list(set(genes))
398
 
@@ -404,8 +431,6 @@ class ViewTargetsView(QtWidgets.QMainWindow):
404
  # Set first item without triggering updates
405
  if self.combo_box_gene.count() > 0:
406
  self.combo_box_gene.setCurrentIndex(0)
407
-
408
- self.logger.debug(f"Added {len(unique_genes)} unique genes to combo box")
409
 
410
  # Re-enable UI updates
411
  self.combo_box_gene.setUpdatesEnabled(True)
@@ -429,22 +454,26 @@ class ViewTargetsView(QtWidgets.QMainWindow):
429
 
430
  def update_gene_viewer(self, sequence, features=None):
431
  """Update both text editor and DNA feature viewer"""
432
- # Update text editor
433
- self.text_edit_gene_viewer.clear()
434
- doc = QTextDocument()
435
- doc.setHtml(sequence)
436
- self.text_edit_gene_viewer.setDocument(doc)
437
-
438
- # Get start position from line edit
439
  try:
440
- start_pos = int(self.line_edit_start_location.text())
441
- except (ValueError, TypeError):
442
- start_pos = 1
443
-
444
- # Update DNA feature viewer
445
- if features is None:
446
- features = []
447
- self.dna_feature_viewer.set_data(sequence, features, start_pos)
 
 
 
 
 
 
 
 
 
 
 
448
 
449
  def select_all_guides(self, select):
450
  for row in range(self.table_guides.rowCount()):
@@ -559,40 +588,256 @@ class ViewTargetsView(QtWidgets.QMainWindow):
559
  self.logger.error(f"Error showing details: {str(e)}")
560
  show_error(self.settings, "Error showing details", str(e))
561
 
562
- def _on_sequence_selected(self, start, end):
563
- """Handle sequence selection in DNA feature viewer"""
564
- self.line_edit_start_location.setText(str(start))
565
- self.line_edit_stop_location.setText(str(end))
566
 
567
- def highlight_guides_in_viewer(self, guides_to_highlight, sequence):
568
- """Highlight guides in viewer"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  try:
570
- for guide in guides_to_highlight:
571
- sequence_to_find = guide['sequence']
572
- strand = guide['strand']
 
 
573
 
574
- if strand == '-':
575
- sequence_to_find = str(Seq(sequence_to_find).reverse_complement())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
 
577
- sequence_upper = sequence.upper()
578
- target_upper = sequence_to_find.upper()
 
 
 
 
579
 
580
- pos = sequence_upper.find(target_upper)
581
- if pos != -1:
582
- # Set color based on strand
583
- color = QColor(255, 0, 0, 100) if strand == '-' else QColor(0, 255, 0, 100)
584
-
585
- # Highlight sequence in viewer
586
- self.dna_feature_viewer.sequence_viewer.highlight_sequence(
587
- pos,
588
- pos + len(sequence_to_find) - 1,
589
- color
590
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
 
592
  except Exception as e:
593
- self.logger.error(f"Error highlighting guides: {str(e)}")
594
- show_error(self.settings, "Error highlighting guides", str(e))
595
-
596
- def clear_highlights(self):
597
- """Clear highlights in viewer"""
598
- self.dna_feature_viewer.sequence_viewer.clear_highlights()
 
1
  from typing import Optional
2
  from PyQt6 import QtWidgets, uic
3
+ from PyQt6.QtWidgets import QTableWidgetItem, QAbstractItemView, QMessageBox, QApplication, QDialog
4
+ from PyQt6.QtGui import QTextDocument, QKeySequence, QColor
5
+ from PyQt6.QtCore import Qt, pyqtSignal, QEvent
6
  from utils.ui import show_error
7
  import traceback
8
+ from views.dna_viewer.dna_feature_viewer import DNAFeatureViewer
9
+ from .dialogs.base_insertion_dialog import BaseInsertionDialog
10
 
11
  class ViewTargetsView(QtWidgets.QMainWindow):
12
  # Define the signal
 
22
  def init_ui(self):
23
  try:
24
  uic.loadUi(self.settings.get_ui_dir_path() + '/view_targets.ui', self)
25
+ self.dna_feature_viewer = DNAFeatureViewer(parent=self)
26
  self._init_ui_components()
27
+ self._set_styles() # Add style initialization
28
  except Exception as e:
29
  show_error(self.settings, "Error initializing ViewTargetsView", str(e))
30
 
31
+ def _set_styles(self):
32
+ """Apply the global groupbox style"""
33
+ try:
34
+ style = self.settings.get_groupbox_style()
35
+ for groupbox in self.findChildren(QtWidgets.QGroupBox):
36
+ groupbox.setStyleSheet(style)
37
+ except Exception as e:
38
+ self.logger.error(f"Error setting styles: {str(e)}")
39
+
40
  def _init_ui_components(self):
41
  self._init_grpGuideViewer()
42
  self._init_grpGuideAnalysis()
 
87
  self.push_button_cotargeting = self._find_widget('pbtnCoTargeting', QtWidgets.QPushButton)
88
 
89
  def _init_grpGeneViewer(self):
90
+ """Initialize gene viewer group"""
91
  self.push_button_highlight_guides = self._find_widget('pbtnHighlightGuides', QtWidgets.QPushButton)
92
  self.push_button_clear_guides = self._find_widget('pbtnClearGuides', QtWidgets.QPushButton)
93
  self.line_edit_start_location = self._find_widget('ledStartLocation', QtWidgets.QLineEdit)
 
95
  self.push_button_change_location = self._find_widget('pbtnChangeLocation', QtWidgets.QPushButton)
96
  self.text_edit_gene_viewer = self._find_widget('txtedGeneViewer', QtWidgets.QTextEdit)
97
  self.push_button_reset_location = self._find_widget('pbtnResetLocation', QtWidgets.QPushButton)
98
+ self.label_sequence_legend = self._find_widget('lblSequenceLegend', QtWidgets.QLabel)
99
  self.check_box_view_exons_only = self._find_widget('chkViewExonsOnly', QtWidgets.QCheckBox)
100
 
101
+ # Hide the text editor since we're using the DNA feature viewer
102
+ self.text_edit_gene_viewer.hide()
103
 
104
+ try:
105
+ # Get the layout of the gene viewer group
106
+ gene_viewer_group = self.findChild(QtWidgets.QGroupBox, 'grpGeneViewer')
107
+ gene_viewer_layout = gene_viewer_group.layout()
108
+
109
+ # Find the row index of the text editor
110
+ text_editor_row = -1
111
+ for i in range(gene_viewer_layout.rowCount()):
112
+ item = gene_viewer_layout.itemAtPosition(i, 0)
113
+ if item and item.widget() == self.text_edit_gene_viewer:
114
+ text_editor_row = i
115
+ break
116
+
117
+ if text_editor_row != -1:
118
+ # Remove the text editor from the layout
119
+ gene_viewer_layout.removeWidget(self.text_edit_gene_viewer)
120
+ # Add DNA feature viewer in its place
121
+ gene_viewer_layout.addWidget(self.dna_feature_viewer, text_editor_row, 0, 1, -1)
122
+
123
+ # Set size policy to make the DNA viewer expand
124
+ self.dna_feature_viewer.setSizePolicy(
125
+ QtWidgets.QSizePolicy.Policy.Expanding,
126
+ QtWidgets.QSizePolicy.Policy.Expanding
127
+ )
128
+
129
+ self.dna_feature_viewer.view.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
130
+ self.dna_feature_viewer.view.installEventFilter(self)
131
+ self.dna_feature_viewer.installEventFilter(self)
132
+
133
+ # Store current sequence for editing
134
+ self._current_sequence = ""
135
+
136
+ except Exception as e:
137
+ self.logger.error(f"Error in _init_grpGeneViewer: {str(e)}")
138
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
139
 
140
  def _find_widget(self, name: str, widget_type: type) -> Optional[QtWidgets.QWidget]:
141
  widget = self.findChild(widget_type, name)
 
143
  self.logger.warning(f"Widget '{name}' not found in UI file.")
144
  return widget
145
 
146
+ def display_guides_in_table(self, guides, suppress_updates=False):
147
  try:
148
  self._all_guides = guides
 
149
  selected_text = self.combo_box_gene.currentText()
150
 
151
  # First filter by position/feature
 
166
  filtered_guides.append(guide)
167
  else:
168
  filtered_guides = self._all_guides
169
+ self.logger.debug("No filtering applied, using all guides")
170
 
171
  # Apply additional filters
172
  final_guides = []
 
186
 
187
  # Update table with new guides
188
  total_rows = len(final_guides)
 
189
 
190
+ # Completely freeze UI only if not suppressing updates
191
+ if not suppress_updates:
192
+ self.setUpdatesEnabled(False)
193
+ self.table_guides.setUpdatesEnabled(False)
194
+ self.table_guides.setSortingEnabled(False)
195
+ self.table_guides.setVisible(False)
196
 
197
  try:
198
  # Clear and resize table
 
266
  guide_viewer_group.setMinimumWidth(essential_columns_width + 50) # Add some padding for scrollbar
267
 
268
  finally:
269
+ # Re-enable UI only if not suppressing updates
270
+ if not suppress_updates:
271
+ self.table_guides.setVisible(True)
272
+ self.table_guides.setUpdatesEnabled(True)
273
+ self.setUpdatesEnabled(True)
274
+ self.table_guides.setSortingEnabled(True)
275
 
276
  except Exception as e:
277
  self.logger.error(f"Error in display_guides: {str(e)}")
 
420
  # Clear existing items efficiently
421
  self.combo_box_gene.clear()
422
 
 
 
 
423
  # Use a set to ensure uniqueness
424
  unique_genes = list(set(genes))
425
 
 
431
  # Set first item without triggering updates
432
  if self.combo_box_gene.count() > 0:
433
  self.combo_box_gene.setCurrentIndex(0)
 
 
434
 
435
  # Re-enable UI updates
436
  self.combo_box_gene.setUpdatesEnabled(True)
 
454
 
455
  def update_gene_viewer(self, sequence, features=None):
456
  """Update both text editor and DNA feature viewer"""
 
 
 
 
 
 
 
457
  try:
458
+ # Update text editor
459
+ self.text_edit_gene_viewer.clear()
460
+ doc = QTextDocument()
461
+ doc.setHtml(sequence)
462
+ self.text_edit_gene_viewer.setDocument(doc)
463
+
464
+ # Get start position from line edit
465
+ try:
466
+ start_pos = int(self.line_edit_start_location.text())
467
+ except (ValueError, TypeError):
468
+ start_pos = 1
469
+
470
+ # Update DNA feature viewer
471
+ if features is None:
472
+ features = []
473
+ self.dna_feature_viewer.set_data(sequence, features, start_pos)
474
+
475
+ except Exception as e:
476
+ self.logger.error(f"Error updating gene viewer: {str(e)}")
477
 
478
  def select_all_guides(self, select):
479
  for row in range(self.table_guides.rowCount()):
 
588
  self.logger.error(f"Error showing details: {str(e)}")
589
  show_error(self.settings, "Error showing details", str(e))
590
 
591
+ def clear_highlights(self):
592
+ """Clear highlights in viewer"""
593
+ self.dna_feature_viewer.sequence_viewer.clear_highlights()
 
594
 
595
+ def eventFilter(self, obj, event):
596
+ """Handle key press events"""
597
+ if event.type() == QEvent.Type.KeyPress:
598
+ # Check for copy command
599
+ if event.matches(QKeySequence.StandardKey.Copy):
600
+ self._handle_copy()
601
+ return True
602
+
603
+ # Handle delete/backspace
604
+ if event.key() in [Qt.Key.Key_Delete, Qt.Key.Key_Backspace]:
605
+ return self._handle_delete()
606
+
607
+ # Handle only valid base pair keys
608
+ if event.text():
609
+ valid_bases = set('ATGCRYMKSWBDHVNatgcrymkswbdhvn')
610
+ if event.text() in valid_bases:
611
+ return self._handle_insert()
612
+
613
+ return super().eventFilter(obj, event)
614
+
615
+ def _handle_copy(self):
616
+ """Copy selected sequence to clipboard"""
617
  try:
618
+ start = self.dna_feature_viewer.insertion_zone.selection_start
619
+ end = self.dna_feature_viewer.insertion_zone.current_cursor_pos
620
+
621
+ if start is not None and end is not None:
622
+ start, end = min(start, end), max(start, end)
623
 
624
+ # Get sequence from DNA viewer instead of stored sequence
625
+ sequence = self.dna_feature_viewer.sequence_viewer.sequence
626
+ if sequence:
627
+ selected_sequence = sequence[start:end]
628
+ if selected_sequence:
629
+ clipboard = QApplication.clipboard()
630
+ clipboard.setText(selected_sequence)
631
+ self.logger.debug(f"Copied sequence: {selected_sequence}")
632
+ else:
633
+ self.logger.warning("No sequence available to copy")
634
+
635
+ except Exception as e:
636
+ self.logger.error(f"Error copying sequence: {str(e)}")
637
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
638
+
639
+ def _handle_delete(self):
640
+ """Handle deletion of selected base pairs"""
641
+ start = self.dna_feature_viewer.insertion_zone.selection_start
642
+ end = self.dna_feature_viewer.insertion_zone.current_cursor_pos
643
+
644
+ # Check if there's no selection but there are highlighted nucleotides
645
+ if (start is None or end is None or start == end):
646
+ # Look for highlighted nucleotides
647
+ sequence_viewer = self.dna_feature_viewer.sequence_viewer
648
+ highlighted_positions = []
649
+
650
+ for i, nuc in enumerate(sequence_viewer.nucleotides):
651
+ if nuc.is_highlighted and nuc.highlight_color == QColor(100, 150, 255, 100): # Check for selection blue
652
+ # Convert nucleotide index to sequence position (divide by 2 since each base has 2 nucleotides)
653
+ pos = i // 2
654
+ highlighted_positions.append(pos)
655
+
656
+ if highlighted_positions:
657
+ # Use the range of highlighted positions
658
+ start = min(highlighted_positions)
659
+ end = max(highlighted_positions) + 1 # Add 1 to include the last position
660
+ else:
661
+ QMessageBox.warning(
662
+ self,
663
+ "No Selection",
664
+ "Please select the bases to be removed and then press Delete",
665
+ QMessageBox.StandardButton.Ok
666
+ )
667
+ return True
668
+
669
+ start, end = min(start, end), max(start, end)
670
+ num_bases = end - start
671
+
672
+ reply = QMessageBox.question(
673
+ self,
674
+ "Confirm Deletion",
675
+ f"Delete {num_bases} bp?",
676
+ QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
677
+ QMessageBox.StandardButton.Cancel
678
+ )
679
+
680
+ if reply == QMessageBox.StandardButton.Ok:
681
+ # Store current highlights before deletion
682
+ guide_highlights = []
683
+ sequence_viewer = self.dna_feature_viewer.sequence_viewer
684
+ for i, nuc in enumerate(sequence_viewer.nucleotides):
685
+ if nuc.is_highlighted:
686
+ # Only store guide highlights (red/green), not selection highlights (blue)
687
+ if nuc.highlight_color not in [QColor(200, 200, 255, 100), QColor(100, 150, 255, 100)]:
688
+ # Store position and color
689
+ guide_highlights.append({
690
+ 'pos': i,
691
+ 'color': nuc.highlight_color,
692
+ 'strand': '-' if i % 2 else '+' # Odd indices are negative strand
693
+ })
694
+
695
+ # Create new sequence
696
+ new_sequence = self._current_sequence[:start] + self._current_sequence[end:]
697
+ self._current_sequence = new_sequence
698
+
699
+ # Clear all highlights before updating viewer
700
+ self.dna_feature_viewer.sequence_viewer.clear_highlights()
701
+
702
+ # Update viewer with new sequence
703
+ self.update_gene_viewer(new_sequence)
704
+
705
+ # Reapply guide highlights, adjusting positions for deleted section
706
+ for highlight in guide_highlights:
707
+ orig_pos = highlight['pos']
708
+ # Calculate new position after deletion
709
+ if orig_pos < start * 2: # Multiply by 2 because each base has two nucleotides
710
+ new_pos = orig_pos
711
+ elif orig_pos > end * 2:
712
+ new_pos = orig_pos - ((end - start) * 2) # Adjust for deleted section
713
+ else:
714
+ continue # Skip highlights in deleted region
715
+
716
+ # Apply highlight to new position
717
+ if new_pos < len(sequence_viewer.nucleotides):
718
+ nuc = sequence_viewer.nucleotides[new_pos]
719
+ nuc.is_highlighted = True
720
+ nuc.highlight_color = highlight['color']
721
+ nuc.update()
722
+
723
+ # Calculate cursor position coordinates
724
+ line_number = start // self.dna_feature_viewer.sequence_viewer.bases_per_line
725
+ cursor_pos_in_line = start % self.dna_feature_viewer.sequence_viewer.bases_per_line
726
+
727
+ # Calculate exact cursor coordinates
728
+ cursor_x = self.dna_feature_viewer.sequence_viewer.strand_margin + (cursor_pos_in_line * self.dna_feature_viewer.sequence_viewer.base_width)
729
+ cursor_y = (line_number * self.dna_feature_viewer.sequence_viewer.line_spacing) + (self.dna_feature_viewer.sequence_viewer.line_height * 0.1)
730
+ cursor_height = self.dna_feature_viewer.sequence_viewer.line_height * 2 + 6
731
+
732
+ # Position cursor at start of deleted region
733
+ self.dna_feature_viewer.insertion_zone.sequence_cursor.set_position(
734
+ cursor_x,
735
+ cursor_y,
736
+ cursor_height
737
+ )
738
+ self.dna_feature_viewer.insertion_zone.sequence_cursor.show()
739
+
740
+ # Update stored cursor position
741
+ self.dna_feature_viewer.insertion_zone.current_cursor_pos = start
742
+
743
+ # Clear selection states
744
+ self.dna_feature_viewer.sequence_viewer.selection_start = None
745
+ self.dna_feature_viewer.sequence_viewer.selection_end = None
746
+ self.dna_feature_viewer.sequence_viewer.selection_active = False
747
+ self.dna_feature_viewer.insertion_zone.selection_start = None
748
+ self.dna_feature_viewer.insertion_zone.selection_end = None
749
+ self.dna_feature_viewer.insertion_zone.selection_active = False
750
+
751
+ # Force update of the view
752
+ scene = self.dna_feature_viewer.scene
753
+ if scene is not None:
754
+ scene.update()
755
+
756
+ # Make sure view maintains focus
757
+ self.dna_feature_viewer.view.setFocus()
758
+
759
+ return True
760
+
761
+ def _handle_insert(self):
762
+ """Handle base pair insertion"""
763
+ cursor_pos = self.dna_feature_viewer.insertion_zone.current_cursor_pos
764
+ if cursor_pos is not None:
765
+ dialog = BaseInsertionDialog(self)
766
+ if dialog.exec() == QDialog.DialogCode.Accepted:
767
+ bases = dialog.get_bases()
768
 
769
+ new_sequence = (
770
+ self._current_sequence[:cursor_pos] +
771
+ bases +
772
+ self._current_sequence[cursor_pos:]
773
+ )
774
+ self._current_sequence = new_sequence
775
 
776
+ # Store new cursor position before updating viewer
777
+ new_cursor_pos = cursor_pos + len(bases)
778
+
779
+ # Update viewer with new sequence
780
+ self.update_gene_viewer(new_sequence)
781
+
782
+ # Highlight the newly added bases on both strands
783
+ highlight_color = QColor(100, 150, 255, 100) # Same blue as selection
784
+ # Highlight positive strand
785
+ self.dna_feature_viewer.sequence_viewer.highlight_sequence(
786
+ cursor_pos, # Start at insertion point
787
+ new_cursor_pos - 1, # End at position before new cursor
788
+ highlight_color,
789
+ strand='+'
790
+ )
791
+ # Highlight negative strand
792
+ self.dna_feature_viewer.sequence_viewer.highlight_sequence(
793
+ cursor_pos, # Start at insertion point
794
+ new_cursor_pos - 1, # End at position before new cursor
795
+ highlight_color,
796
+ strand='-'
797
+ )
798
+
799
+ # Calculate cursor position coordinates
800
+ line_number = new_cursor_pos // self.dna_feature_viewer.sequence_viewer.bases_per_line
801
+ cursor_pos_in_line = new_cursor_pos % self.dna_feature_viewer.sequence_viewer.bases_per_line
802
+
803
+ # Calculate exact cursor coordinates
804
+ cursor_x = self.dna_feature_viewer.sequence_viewer.strand_margin + (cursor_pos_in_line * self.dna_feature_viewer.sequence_viewer.base_width)
805
+ cursor_y = (line_number * self.dna_feature_viewer.sequence_viewer.line_spacing) + (self.dna_feature_viewer.sequence_viewer.line_height * 0.1)
806
+ cursor_height = self.dna_feature_viewer.sequence_viewer.line_height * 2 + 6
807
+
808
+ # Position cursor after inserted bases
809
+ self.dna_feature_viewer.insertion_zone.sequence_cursor.set_position(
810
+ cursor_x,
811
+ cursor_y,
812
+ cursor_height
813
+ )
814
+ self.dna_feature_viewer.insertion_zone.sequence_cursor.show()
815
+
816
+ # Update stored cursor position
817
+ self.dna_feature_viewer.insertion_zone.current_cursor_pos = new_cursor_pos
818
+
819
+ # Make sure view maintains focus
820
+ self.dna_feature_viewer.view.setFocus()
821
+
822
+ return True
823
+
824
+ def closeEvent(self, event):
825
+ """Handle cleanup when window is closed"""
826
+ try:
827
+ # Clean up DNA viewer if it exists
828
+ if hasattr(self, 'dna_feature_viewer'):
829
+ self.dna_feature_viewer.closeEvent(event)
830
+
831
+ # Ensure all views release mouse tracking
832
+ for child in self.findChildren(QtWidgets.QWidget):
833
+ if isinstance(child, (QtWidgets.QGraphicsView, QtWidgets.QAbstractScrollArea)):
834
+ child.setMouseTracking(False)
835
+ child.viewport().setMouseTracking(False)
836
+ if child.hasMouseTracking():
837
+ child.releaseMouse()
838
 
839
  except Exception as e:
840
+ if hasattr(self, 'logger'):
841
+ self.logger.error(f"Error in closeEvent: {str(e)}")
842
+
843
+ super().closeEvent(event)
 
 
src/views/dialogs/base_insertion_dialog.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton
2
+
3
+ class BaseInsertionDialog(QDialog):
4
+ """Dialog for inserting base pairs"""
5
+ def __init__(self, parent=None):
6
+ super().__init__(parent)
7
+ self.setWindowTitle("Insert Base Pairs")
8
+ self.setModal(True)
9
+
10
+ self.valid_bases = set('ATGCRYMKSWBDHVNatgcrymkswbdhvn')
11
+
12
+ layout = QVBoxLayout(self)
13
+
14
+ label = QLabel(
15
+ "Enter base pairs to insert:\n"
16
+ "A (Adenine), T (Thymine), G (Guanine), C (Cytosine)\n"
17
+ "R (A/G), Y (C/T), M (A/C), K (G/T), S (G/C), W (A/T)\n"
18
+ "H (A/C/T), B (G/C/T), V (G/C/A), D (G/A/T), N (Any)"
19
+ )
20
+ layout.addWidget(label)
21
+
22
+ self.input_field = QLineEdit()
23
+ self.input_field.setPlaceholderText("e.g., ATGC, RYKMSWBDHVN")
24
+ self.input_field.textChanged.connect(self._validate_input)
25
+ layout.addWidget(self.input_field)
26
+
27
+ button_layout = QHBoxLayout()
28
+
29
+ self.insert_button = QPushButton("Insert")
30
+ self.insert_button.clicked.connect(self.accept)
31
+ self.insert_button.setEnabled(False) # Disabled until valid input
32
+
33
+ cancel_button = QPushButton("Cancel")
34
+ cancel_button.clicked.connect(self.reject)
35
+
36
+ button_layout.addWidget(self.insert_button)
37
+ button_layout.addWidget(cancel_button)
38
+ layout.addLayout(button_layout)
39
+
40
+ self.setMinimumWidth(400)
41
+
42
+ def _validate_input(self, text):
43
+ """Validate input and filter invalid characters"""
44
+ # Remove any invalid characters
45
+ valid_text = ''.join(c for c in text if c in self.valid_bases)
46
+
47
+ # If text changed, update the field
48
+ if valid_text != text:
49
+ self.input_field.setText(valid_text)
50
+
51
+ # Enable/disable insert button based on input
52
+ self.insert_button.setEnabled(bool(valid_text))
53
+
54
+ def get_bases(self):
55
+ """Return the entered base pairs preserving case"""
56
+ return self.input_field.text()
src/views/dna_viewer/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Empty file to make the directory a Python package
src/views/dna_viewer/components/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Empty file to make the directory a Python package
src/views/dna_viewer/components/feature_viewer.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt6.QtWidgets import QGraphicsObject
2
+ from PyQt6.QtCore import Qt, QRectF, pyqtSignal, QPointF
3
+ from PyQt6.QtGui import QBrush, QColor, QFont
4
+
5
+ class FeatureViewer(QGraphicsObject):
6
+ """Component for displaying DNA features like genes and exons"""
7
+ cursor_position_changed = pyqtSignal(int)
8
+
9
+ def __init__(self, parent=None):
10
+ super().__init__(parent)
11
+ self.sequence = ""
12
+ self.features = []
13
+ self.start_pos = 0
14
+ self.base_width = 15
15
+ self.bases_per_line = 70
16
+ self.feature_height = 20
17
+ self.line_height = 25
18
+ self.feature_spacing = 2 # Spacing between features and strands
19
+ self.setAcceptHoverEvents(True)
20
+
21
+ # Add logger
22
+ import logging
23
+ self.logger = logging.getLogger(__name__)
24
+
25
+ def set_data(self, sequence, features, start_pos):
26
+ """Set sequence and features data"""
27
+ self.sequence = sequence
28
+ self.features = sorted(features, key=lambda x: x['start'])
29
+ self.start_pos = start_pos
30
+ self.update()
31
+
32
+ def paint(self, painter, option, widget):
33
+ """Paint the features"""
34
+ if not self.features or not self.sequence:
35
+ return
36
+
37
+ # Process each line of sequence
38
+ current_pos = 0
39
+ while current_pos < len(self.sequence):
40
+ line_text = self.sequence[current_pos:current_pos + self.bases_per_line]
41
+ line_num = current_pos // self.bases_per_line
42
+
43
+ # Calculate y position to be directly below negative strand
44
+ y_pos = line_num * self.line_height * 2
45
+ feature_y = y_pos + self.line_height * 2 # Position below negative strand
46
+
47
+ # Calculate sequence width for this line
48
+ sequence_width = len(line_text) * self.base_width
49
+
50
+ # Draw features
51
+ for feature in self.features:
52
+ try:
53
+ # Calculate relative positions within current line
54
+ feature_start = feature['start'] - current_pos
55
+ feature_end = feature['end'] - current_pos
56
+
57
+ # Skip if feature is not in current line
58
+ if feature_end < 0 or feature_start >= self.bases_per_line:
59
+ continue
60
+
61
+ # Clip to line boundaries
62
+ feature_start = max(0, feature_start)
63
+ feature_end = min(self.bases_per_line, feature_end)
64
+
65
+ # Calculate pixel positions
66
+ x_start = feature_start * self.base_width
67
+ x_end = feature_end * self.base_width
68
+
69
+ # Create rectangle for feature
70
+ feature_rect = QRectF(
71
+ x_start,
72
+ feature_y,
73
+ x_end - x_start,
74
+ self.feature_height
75
+ )
76
+
77
+ # Set color based on feature type
78
+ if feature.get('type') == 'exon':
79
+ color = QColor(100, 180, 255) # Light blue for exons
80
+ else:
81
+ color = QColor(255, 140, 0) # Orange for genes
82
+
83
+ # Draw feature rectangle
84
+ painter.setBrush(QBrush(color))
85
+ painter.setPen(Qt.PenStyle.NoPen)
86
+ painter.drawRect(feature_rect)
87
+
88
+ # Draw label if enough space
89
+ label = feature.get('name', '')
90
+ text_width = painter.fontMetrics().horizontalAdvance(label)
91
+ if (x_end - x_start) > text_width:
92
+ text_x = x_start + ((x_end - x_start) - text_width) / 2
93
+ text_y = feature_y + self.feature_height/2 + 4
94
+ painter.setPen(Qt.GlobalColor.white)
95
+ painter.setFont(QFont("Arial", 8))
96
+ painter.drawText(QPointF(text_x, text_y), label)
97
+
98
+ except Exception as e:
99
+ self.logger.error(f"Error drawing feature: {str(e)}")
100
+ continue
101
+
102
+ current_pos += self.bases_per_line
103
+
104
+ def boundingRect(self):
105
+ """Return the bounding rectangle of the component"""
106
+ if not self.sequence:
107
+ return QRectF()
108
+
109
+ # Calculate exact width based on sequence length
110
+ last_line_length = len(self.sequence) % self.bases_per_line
111
+ if last_line_length == 0:
112
+ last_line_length = self.bases_per_line
113
+ width = max(self.base_width * self.bases_per_line,
114
+ self.base_width * last_line_length) + 100
115
+
116
+ # Calculate height for actual sequence lines
117
+ total_lines = (len(self.sequence) + self.bases_per_line - 1) // self.bases_per_line
118
+ height = total_lines * self.line_height * 2
119
+
120
+ return QRectF(0, 0, width, height)
121
+
122
+ def mousePressEvent(self, event):
123
+ """Handle mouse press to show insertion point"""
124
+ if event.button() == Qt.MouseButton.LeftButton:
125
+ # Calculate position based on click location
126
+ local_pos = event.pos()
127
+ line_number = int(local_pos.y() // (self.line_height * 2))
128
+ base_position = int(local_pos.x() // self.base_width)
129
+
130
+ # Calculate absolute position
131
+ position = self.start_pos + line_number * self.bases_per_line + base_position
132
+
133
+ # Emit cursor position
134
+ self.cursor_position_changed.emit(position)
135
+
136
+ event.accept()
137
+
138
+ def mouseMoveEvent(self, event):
139
+ """Handle mouse move to update insertion point"""
140
+ if event.buttons() & Qt.MouseButton.LeftButton:
141
+ # Calculate position based on mouse location
142
+ local_pos = event.pos()
143
+ line_number = int(local_pos.y() // (self.line_height * 2))
144
+ base_position = int(local_pos.x() // self.base_width)
145
+
146
+ # Calculate absolute position
147
+ position = self.start_pos + line_number * self.bases_per_line + base_position
148
+
149
+ # Emit cursor position
150
+ self.cursor_position_changed.emit(position)
151
+
152
+ event.accept()
src/views/dna_viewer/components/nucleotide_item.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt6.QtWidgets import QGraphicsObject
2
+ from PyQt6.QtCore import Qt, QRectF, pyqtSignal
3
+ from PyQt6.QtGui import QFont, QColor
4
+
5
+ class NucleotideItem(QGraphicsObject):
6
+ """Component for displaying individual nucleotides in the gene sequence viewer"""
7
+ clicked = pyqtSignal(object)
8
+
9
+ def __init__(self, nucleotide, x, y, width, is_uppercase=True, is_complement=False, is_padding=False, parent=None):
10
+ """Initialize nucleotide item
11
+
12
+ Args:
13
+ nucleotide (str): The nucleotide base (A, T, G, C)
14
+ x (float): X position
15
+ y (float): Y position
16
+ width (float): Width of nucleotide
17
+ is_uppercase (bool): Whether nucleotide should be uppercase
18
+ is_complement (bool): Whether this is a complement strand nucleotide
19
+ is_padding (bool): Whether this nucleotide is part of padding sequence
20
+ parent (QGraphicsObject): Parent item
21
+ """
22
+ super().__init__(parent)
23
+ self.nucleotide = nucleotide
24
+ self.setPos(x, y)
25
+ self.width = width
26
+ self.base_width = width
27
+ self.height = 20
28
+ self.is_uppercase = is_uppercase
29
+ self.is_complement = is_complement
30
+ self.is_padding = is_padding
31
+ self.is_highlighted = False
32
+ self.highlight_color = None
33
+
34
+ # Enable hover events
35
+ self.setAcceptHoverEvents(True)
36
+
37
+ def paint(self, painter, option, widget):
38
+ """Paint the nucleotide"""
39
+ try:
40
+ # Draw highlight background if highlighted
41
+ if self.is_highlighted and self.highlight_color:
42
+ painter.save()
43
+ painter.fillRect(self.boundingRect(), self.highlight_color)
44
+ painter.restore()
45
+
46
+ # Draw nucleotide centered
47
+ painter.setFont(QFont("Courier", 12))
48
+
49
+ # Use grey for padding sequence, black for all other nucleotides
50
+ if self.is_padding:
51
+ painter.setPen(QColor(100, 100, 100))
52
+ else:
53
+ painter.setPen(Qt.GlobalColor.black)
54
+
55
+ # Get complement nucleotide if needed
56
+ display_nucleotide = self._get_complement() if self.is_complement else self.nucleotide
57
+
58
+ # Draw text centered
59
+ painter.drawText(self.boundingRect(), Qt.AlignmentFlag.AlignCenter, display_nucleotide)
60
+
61
+ except Exception as e:
62
+ print(f"Error in paint: {str(e)}")
63
+
64
+ def boundingRect(self):
65
+ """Return the bounding rectangle for the nucleotide"""
66
+ return QRectF(0, 0, self.width, self.height)
67
+
68
+ def mousePressEvent(self, event):
69
+ """Handle mouse press events"""
70
+ if event.button() == Qt.MouseButton.LeftButton:
71
+ sequence_viewer = self.parent()
72
+ if sequence_viewer:
73
+ pos = sequence_viewer.get_nucleotide_position(self)
74
+ local_x = event.pos().x()
75
+
76
+ # Calculate position relative to letter boundaries
77
+ text_x = (self.width - self.base_width) / 2
78
+ relative_x = local_x - text_x
79
+
80
+ # Determine cursor position
81
+ if relative_x <= 0: # Before letter
82
+ cursor_pos = pos
83
+ elif relative_x >= self.base_width: # After letter
84
+ cursor_pos = pos + 1
85
+ else: # On letter
86
+ cursor_pos = pos + (1 if relative_x > self.base_width / 2 else 0)
87
+
88
+ # Update selection and cursor
89
+ sequence_viewer.selection_start = cursor_pos
90
+ sequence_viewer.selection_end = cursor_pos
91
+ sequence_viewer.selection_active = True
92
+
93
+ sequence_viewer.cursor_position_changed.emit(cursor_pos)
94
+ sequence_viewer._update_selection()
95
+ self.update()
96
+
97
+ event.accept()
98
+
99
+ def _get_complement(self):
100
+ """Get complement nucleotide"""
101
+ complement_map = {
102
+ 'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G',
103
+ 'a': 't', 't': 'a', 'g': 'c', 'c': 'g',
104
+ 'K': 'M', 'M': 'K', 'R': 'Y', 'Y': 'R',
105
+ 'k': 'm', 'm': 'k', 'r': 'y', 'y': 'r',
106
+ 'S': 'S', 's': 's'
107
+ }
108
+ return complement_map.get(self.nucleotide, self.nucleotide)
src/views/dna_viewer/components/ruler.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from PyQt6.QtWidgets import QGraphicsScene, QGraphicsLineItem, QGraphicsSimpleTextItem
3
+ from PyQt6.QtCore import QRectF, Qt
4
+ from PyQt6.QtGui import QPen, QColor, QFont, QBrush
5
+
6
+ class Ruler(QGraphicsScene):
7
+ """Component for displaying the ruler with position markers"""
8
+
9
+ def __init__(self, parent=None):
10
+ super().__init__(parent)
11
+ self.logger = logging.getLogger(__name__)
12
+ self.base_width = 15
13
+ self.strand_margin = 40 # Width for 5' and 3' indicators
14
+ self.ruler_height = 30
15
+ self.ruler_color = QColor(0, 120, 215) # Blue
16
+
17
+ # Set background color to match interface
18
+ background_color = QColor(240, 240, 240)
19
+ self.setBackgroundBrush(QBrush(background_color))
20
+
21
+ # Ensure the background fills the entire scene
22
+ self.setSceneRect(QRectF(0, 0, 1000, self.ruler_height - 15))
23
+
24
+ # Tick mark settings
25
+ self.major_tick_height = 10 # Height for major ticks (multiples of 10)
26
+ self.medium_tick_height = 7 # Height for medium ticks (multiples of 5)
27
+ self.minor_tick_height = 4 # Height for minor ticks
28
+
29
+ self.ruler_y = 15 # Y position of main ruler line
30
+
31
+ # Initialize tracker line as None - we'll create it when needed
32
+ self.tracker_line = None
33
+
34
+ def create_ruler(self, bases_per_line):
35
+ """Create ruler with position markers
36
+
37
+ Args:
38
+ bases_per_line (int): Number of bases per line in sequence viewer
39
+ """
40
+ try:
41
+ self.clear() # Clear everything
42
+
43
+ # Create horizontal blue line aligned with sequence
44
+ ruler_line = QGraphicsLineItem(
45
+ self.strand_margin, self.ruler_y,
46
+ bases_per_line * self.base_width + self.strand_margin, self.ruler_y
47
+ )
48
+ ruler_line.setPen(QPen(self.ruler_color, 1))
49
+ self.addItem(ruler_line)
50
+
51
+ # Add tick marks and numbers
52
+ for i in range(0, bases_per_line):
53
+ x_pos = i * self.base_width + self.strand_margin + self.base_width/2
54
+ pos_1_based = i + 1
55
+
56
+ # Determine tick properties based on position
57
+ if pos_1_based % 10 == 0: # Major ticks (every 10)
58
+ tick_height = self.major_tick_height
59
+ tick_start = 10
60
+ # Add number
61
+ text = QGraphicsSimpleTextItem(str(pos_1_based))
62
+ text.setFont(QFont("Arial", 8))
63
+ text_width = text.boundingRect().width()
64
+ text.setPos(x_pos - text_width/2, 0)
65
+ self.addItem(text)
66
+ elif pos_1_based % 5 == 0: # Medium ticks (every 5)
67
+ tick_height = self.medium_tick_height
68
+ tick_start = 11
69
+ else: # Minor ticks
70
+ tick_height = self.minor_tick_height
71
+ tick_start = 13
72
+
73
+ # Create tick mark
74
+ tick = QGraphicsLineItem(
75
+ x_pos, tick_start,
76
+ x_pos, tick_start + tick_height
77
+ )
78
+ tick.setPen(QPen(self.ruler_color, 1))
79
+ self.addItem(tick)
80
+
81
+ # Create new tracker line
82
+ self.tracker_line = QGraphicsLineItem()
83
+ pen = QPen(self.ruler_color)
84
+ pen.setWidth(2)
85
+ pen.setStyle(Qt.PenStyle.SolidLine)
86
+ self.tracker_line.setPen(pen)
87
+ self.tracker_line.setZValue(100)
88
+ self.tracker_line.hide()
89
+ self.addItem(self.tracker_line)
90
+
91
+ except Exception as e:
92
+ self.logger.error(f"Error creating ruler: {str(e)}")
93
+
94
+ def boundingRect(self):
95
+ """Return the bounding rectangle of the ruler"""
96
+ return self.sceneRect()
97
+
98
+ def update_tracker_position(self, x_pos):
99
+ """Update the position of the tracker line"""
100
+ try:
101
+ if self.tracker_line is None:
102
+ # Create tracker line if it doesn't exist
103
+ self.tracker_line = QGraphicsLineItem()
104
+ pen = QPen(self.ruler_color)
105
+ pen.setWidth(2)
106
+ pen.setStyle(Qt.PenStyle.SolidLine)
107
+ self.tracker_line.setPen(pen)
108
+ self.tracker_line.setZValue(100)
109
+ self.addItem(self.tracker_line)
110
+
111
+ # Ensure x_pos is within bounds
112
+ scene_rect = self.sceneRect()
113
+ x_pos = max(self.strand_margin, min(x_pos, scene_rect.width() - self.strand_margin))
114
+
115
+ # Set tracker line position
116
+ self.tracker_line.setLine(x_pos, 0, x_pos, self.ruler_height)
117
+ self.tracker_line.show()
118
+ self.update()
119
+
120
+ except Exception as e:
121
+ self.logger.error(f"Error updating tracker position: {str(e)}")
src/views/dna_viewer/components/sequence_cursor.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt6.QtWidgets import QGraphicsLineItem
2
+ from PyQt6.QtCore import Qt, QRectF, QPointF
3
+ from PyQt6.QtGui import QPen, QColor, QPainter
4
+
5
+ class SequenceCursor(QGraphicsLineItem):
6
+ """A vertical I-beam cursor for DNA sequence that spans between strands"""
7
+
8
+ def __init__(self, parent=None):
9
+ super().__init__(parent)
10
+
11
+ # Use a solid blue line
12
+ self.pen = QPen(QColor(0, 100, 255))
13
+ self.pen.setWidth(2)
14
+ self.pen.setStyle(Qt.PenStyle.SolidLine)
15
+ self.setPen(self.pen)
16
+
17
+ # Keep cursor on top
18
+ self.setZValue(9999)
19
+
20
+ # Don't make cursor selectable/focusable
21
+ self.setAcceptHoverEvents(False)
22
+ self.setAcceptedMouseButtons(Qt.MouseButton.NoButton)
23
+
24
+ # Disable caching for immediate updates
25
+ self.setCacheMode(QGraphicsLineItem.CacheMode.NoCache)
26
+
27
+ def set_position(self, x, y, height):
28
+ """Position the cursor at given coordinates"""
29
+ self.setLine(x, y, x, y + height)
30
+ self.update()
31
+
32
+ def paint(self, painter, option, widget):
33
+ """Draw the I-beam cursor"""
34
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
35
+
36
+ # Draw vertical line
37
+ painter.setPen(self.pen)
38
+ line = self.line()
39
+ painter.drawLine(line)
40
+
41
+ # Draw small horizontal lines at top and bottom using QPointF
42
+ bar_width = 6
43
+
44
+ # Top bar
45
+ top_start = QPointF(line.x1() - bar_width/2, line.y1())
46
+ top_end = QPointF(line.x1() + bar_width/2, line.y1())
47
+ painter.drawLine(top_start, top_end)
48
+
49
+ # Bottom bar
50
+ bottom_start = QPointF(line.x1() - bar_width/2, line.y2())
51
+ bottom_end = QPointF(line.x1() + bar_width/2, line.y2())
52
+ painter.drawLine(bottom_start, bottom_end)
53
+
54
+ def boundingRect(self):
55
+ """Bounding rectangle for the cursor"""
56
+ line = self.line()
57
+ padding = 4
58
+ return QRectF(line.x1() - padding,
59
+ line.y1() - padding,
60
+ padding * 2,
61
+ line.y2() - line.y1() + padding * 2)
src/views/dna_viewer/components/sequence_insertion_zone.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt6.QtWidgets import QGraphicsObject
2
+ from PyQt6.QtCore import Qt, QRectF, pyqtSignal, QPointF, QEvent
3
+ from PyQt6.QtGui import QColor, QPainterPath
4
+ from .sequence_cursor import SequenceCursor
5
+ import logging
6
+
7
+ class SequenceInsertionZone(QGraphicsObject):
8
+ """Handles the interactive zone between base pairs for insertion/deletion"""
9
+
10
+ insertion_point_selected = pyqtSignal(int) # Emits position when zone is clicked
11
+
12
+ def __init__(self, parent=None):
13
+ super().__init__(parent)
14
+ self.logger = logging.getLogger(__name__)
15
+
16
+ self.zone_width = 8
17
+
18
+ # State tracking
19
+ self.hover_position = None
20
+ self.active_zones = [] # List of (x, y, width, height) for each zone
21
+
22
+ self.sequence_length = 0
23
+ self.bases_per_line = 70 # Default value
24
+ self.base_width = 15
25
+ self.strand_margin = 40
26
+ self.line_height = 25
27
+
28
+ self.sequence_cursor = SequenceCursor(self)
29
+ self.sequence_cursor.hide()
30
+ self.current_cursor_pos = None
31
+
32
+ self.setAcceptHoverEvents(True)
33
+ self.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
34
+ self.setFlag(QGraphicsObject.GraphicsItemFlag.ItemIsSelectable, True)
35
+ self.setFlag(QGraphicsObject.GraphicsItemFlag.ItemIsFocusable, True)
36
+ self.setFlag(QGraphicsObject.GraphicsItemFlag.ItemClipsToShape, False)
37
+ self.setZValue(100) # Keep zones above sequence
38
+
39
+ # Add tracking for visible area
40
+ self.visible_rect = QRectF()
41
+ self.scale_factor = 1.0
42
+
43
+ # Add tracking for hover state
44
+ self.last_valid_hover_pos = None
45
+ self.hover_active = False
46
+
47
+ def create_zones(self, sequence_length, base_width, strand_margin, line_height, bases_per_line, line_spacing=None):
48
+ """Create insertion zones between bases"""
49
+ self.active_zones.clear()
50
+ self.line_height = line_height
51
+ self.line_spacing = line_spacing if line_spacing is not None else line_height * 3
52
+ self.bases_per_line = bases_per_line
53
+ self.base_width = base_width
54
+ self.strand_margin = strand_margin
55
+ self.sequence_length = sequence_length
56
+ self.update()
57
+
58
+ def contains(self, point):
59
+ """Override contains to better handle hover detection"""
60
+ # Convert point to local coordinates if needed
61
+ local_point = point
62
+ if isinstance(point, QPointF):
63
+ local_point = self.mapFromScene(point)
64
+
65
+ # Calculate position
66
+ line_number = int(local_point.y() / self.line_spacing)
67
+ x_relative = local_point.x() - self.strand_margin
68
+ position_in_line = int(x_relative / self.base_width)
69
+ absolute_position = (line_number * self.bases_per_line) + position_in_line
70
+
71
+ # Calculate total lines
72
+ total_lines = (self.sequence_length + self.bases_per_line - 1) // self.bases_per_line
73
+
74
+ # Check if position is valid with more precise bounds
75
+ in_x_range = -self.zone_width <= x_relative <= (self.bases_per_line * self.base_width + self.zone_width)
76
+ in_y_range = 0 <= line_number < total_lines
77
+ position_valid = 0 <= absolute_position <= self.sequence_length
78
+
79
+ # Calculate vertical position within line with more generous zones
80
+ y_in_line = local_point.y() % self.line_spacing
81
+
82
+ # Define zones with appropriate overlap and coverage
83
+ zone_height = self.line_height * 1.8
84
+ middle_point = self.line_spacing / 2
85
+
86
+ # Upper zone covers from start to middle + overlap
87
+ upper_zone_start = 0
88
+ upper_zone_end = middle_point + (zone_height / 2)
89
+
90
+ # Lower zone covers from middle - overlap to end
91
+ lower_zone_start = middle_point - (zone_height / 2)
92
+ lower_zone_end = self.line_spacing
93
+
94
+ # Allow interaction in both upper and lower strand regions with overlap
95
+ upper_strand_zone = upper_zone_start <= y_in_line <= upper_zone_end
96
+ lower_strand_zone = lower_zone_start <= y_in_line <= lower_zone_end
97
+ in_vertical_zone = upper_strand_zone or lower_strand_zone
98
+
99
+ return in_x_range and in_y_range and position_valid and in_vertical_zone
100
+
101
+ def hoverMoveEvent(self, event):
102
+ """Handle hover using direct coordinate calculation"""
103
+ pos = event.pos()
104
+
105
+ if self.contains(pos):
106
+ # Calculate snapped position for cursor only
107
+ line_number = int(pos.y() / self.line_spacing)
108
+ x_relative = pos.x() - self.strand_margin
109
+ position_in_line = int(x_relative / self.base_width)
110
+ absolute_position = (line_number * self.bases_per_line) + position_in_line
111
+
112
+ # Update cursor behavior
113
+ if self.hover_position != absolute_position:
114
+ self.hover_position = absolute_position
115
+ self.setCursor(Qt.CursorShape.IBeamCursor)
116
+
117
+ # Update ruler tracker with exact mouse position - no snapping
118
+ if self.scene() and self.scene().parent():
119
+ dna_viewer = self.scene().parent()
120
+ if hasattr(dna_viewer, 'ruler_scene'):
121
+ # Get exact mouse position in scene coordinates
122
+ scene_pos = self.mapToScene(pos)
123
+
124
+ # Adjust for any offset between sequence view and ruler
125
+ ruler_x = scene_pos.x()
126
+
127
+ # Account for margin differences between sequence and ruler
128
+ margin_diff = self.strand_margin - dna_viewer.ruler_scene.strand_margin
129
+ if margin_diff != 0:
130
+ ruler_x -= margin_diff
131
+
132
+ # Update tracker immediately
133
+ dna_viewer.ruler_scene.update_tracker_position(ruler_x)
134
+
135
+ # Force immediate updates
136
+ dna_viewer.ruler_scene.update()
137
+ dna_viewer.ruler_view.viewport().update()
138
+
139
+ # Update scene for smooth rendering
140
+ if self.scene():
141
+ self.scene().update()
142
+ else:
143
+ if self.hover_position is not None:
144
+ self.hover_position = None
145
+ self.setCursor(Qt.CursorShape.ArrowCursor)
146
+
147
+ # Hide ruler tracker when not hovering
148
+ if self.scene() and self.scene().parent():
149
+ dna_viewer = self.scene().parent()
150
+ if hasattr(dna_viewer, 'ruler_scene'):
151
+ dna_viewer.ruler_scene.tracker_line.hide()
152
+ dna_viewer.ruler_scene.update()
153
+ dna_viewer.ruler_view.viewport().update()
154
+
155
+ return super().hoverMoveEvent(event)
156
+
157
+
158
+ def mousePressEvent(self, event):
159
+ """Handle mouse press to only place cursor without highlighting"""
160
+ pos = event.pos()
161
+
162
+ # Calculate position relative to sequence
163
+ x_relative = pos.x() - self.strand_margin
164
+ line_number = int(pos.y() / self.line_spacing)
165
+
166
+ # Calculate nearest space between bases for cursor
167
+ raw_position = x_relative / self.base_width
168
+ cursor_position = round(raw_position) # Round to nearest space
169
+ absolute_position = (line_number * self.bases_per_line) + cursor_position
170
+
171
+ # Confine cursor position within sequence boundaries
172
+ absolute_position = max(0, min(absolute_position, self.sequence_length))
173
+ cursor_position = absolute_position % self.bases_per_line
174
+ line_number = absolute_position // self.bases_per_line
175
+
176
+ # Store selection start but don't highlight yet
177
+ self.selection_start = absolute_position
178
+
179
+ # Reset selection state
180
+ self.selection_active = False
181
+
182
+ # Position cursor at nearest space between bases
183
+ cursor_x = self.strand_margin + (cursor_position * self.base_width)
184
+ cursor_y = (line_number * self.line_spacing) + (self.line_height * 0.1)
185
+ cursor_height = self.line_height * 2 + 5
186
+
187
+ # Show cursor
188
+ self.sequence_cursor.set_position(cursor_x, cursor_y, cursor_height)
189
+ self.sequence_cursor.show()
190
+
191
+ # Store and emit current cursor position
192
+ self.current_cursor_pos = absolute_position
193
+
194
+ # Get DNA feature viewer and emit cursor position change
195
+ if self.scene() and self.scene().parent():
196
+ dna_viewer = self.scene().parent()
197
+ sequence_viewer = dna_viewer.sequence_viewer
198
+
199
+ # Clear selection state and highlights
200
+ sequence_viewer.selection_start = None
201
+ sequence_viewer.selection_end = None
202
+ sequence_viewer.selection_active = False
203
+
204
+ # Clear selection highlights
205
+ selection_color = QColor(100, 150, 255, 100)
206
+ for nuc in sequence_viewer.nucleotides:
207
+ if nuc.highlight_color == selection_color:
208
+ nuc.is_highlighted = False
209
+ nuc.highlight_color = None
210
+ nuc.update()
211
+
212
+ # Emit cursor position change immediately
213
+ sequence_viewer.cursor_position_changed.emit(absolute_position)
214
+
215
+ event.accept()
216
+
217
+ def mouseMoveEvent(self, event):
218
+ """Handle mouse drag for selection"""
219
+ # Only start selection if mouse has moved
220
+ if not self.selection_active:
221
+ # Check if mouse has moved enough to start selection
222
+ initial_pos = event.pos()
223
+ x_relative = initial_pos.x() - self.strand_margin
224
+ raw_position = x_relative / self.base_width
225
+ current_position = round(raw_position)
226
+
227
+ # Only activate selection if mouse has moved to a different position
228
+ if (current_position != self.selection_start // self.bases_per_line):
229
+ self.selection_active = True
230
+
231
+ if self.selection_active:
232
+ pos = event.pos()
233
+
234
+ # Calculate current position
235
+ x_relative = pos.x() - self.strand_margin
236
+ line_number = int(pos.y() / self.line_spacing)
237
+ raw_position = x_relative / self.base_width
238
+ cursor_position = round(raw_position) # For cursor placement
239
+
240
+ # For highlighting, use the same rounding logic as cursor
241
+ highlight_position = cursor_position
242
+
243
+ # Convert to sequence positions
244
+ current_pos = (line_number * self.bases_per_line) + highlight_position
245
+
246
+ # Confine position within sequence boundaries
247
+ current_pos = max(0, min(current_pos, self.sequence_length))
248
+ cursor_position = current_pos % self.bases_per_line
249
+ line_number = current_pos // self.bases_per_line
250
+
251
+ if 0 <= current_pos <= self.sequence_length:
252
+ scene = self.scene()
253
+ if scene and scene.parent():
254
+ dna_viewer = scene.parent()
255
+ sequence_viewer = dna_viewer.sequence_viewer
256
+
257
+ # Update ruler tracker with exact mouse position during selection
258
+ if hasattr(dna_viewer, 'ruler_scene'):
259
+ # Get exact mouse position in scene coordinates
260
+ scene_pos = self.mapToScene(pos)
261
+
262
+ # Adjust for any offset between sequence view and ruler
263
+ ruler_x = scene_pos.x()
264
+
265
+ # Account for margin differences between sequence and ruler
266
+ margin_diff = self.strand_margin - dna_viewer.ruler_scene.strand_margin
267
+ if margin_diff != 0:
268
+ ruler_x -= margin_diff
269
+
270
+ # Update tracker immediately
271
+ dna_viewer.ruler_scene.update_tracker_position(ruler_x)
272
+ dna_viewer.ruler_scene.update()
273
+ dna_viewer.ruler_view.viewport().update()
274
+
275
+ # Clear previous selection highlights
276
+ selection_color = QColor(100, 150, 255, 100)
277
+ for nuc in sequence_viewer.nucleotides:
278
+ if nuc.highlight_color == selection_color:
279
+ nuc.is_highlighted = False
280
+ nuc.highlight_color = None
281
+ nuc.update()
282
+
283
+ # Determine selection range
284
+ if current_pos >= self.selection_start:
285
+ start = self.selection_start
286
+ end = current_pos - 1
287
+ else:
288
+ start = current_pos
289
+ end = self.selection_start - 1
290
+
291
+ try:
292
+ # Only highlight if there's a valid range
293
+ if start <= end:
294
+ for base_idx in range(start, end + 1):
295
+ pos_strand_idx = base_idx * 2
296
+ neg_strand_idx = base_idx * 2 + 1
297
+
298
+ for idx in [pos_strand_idx, neg_strand_idx]:
299
+ if idx < len(sequence_viewer.nucleotides):
300
+ nuc = sequence_viewer.nucleotides[idx]
301
+ if not nuc.is_highlighted or nuc.highlight_color == selection_color:
302
+ nuc.is_highlighted = True
303
+ nuc.highlight_color = selection_color
304
+ nuc.update()
305
+
306
+ # Emit selection signal with adjusted end position for display
307
+ absolute_start = dna_viewer._original_start_pos + start + 1 # Add 1 only for display
308
+ absolute_end = dna_viewer._original_start_pos + end + 1 # Add 1 only for display
309
+ sequence_viewer.sequence_selected.emit(absolute_start, absolute_end)
310
+
311
+ # Update scene and cursor
312
+ if scene:
313
+ scene.update()
314
+
315
+ # Position cursor at nearest space between bases
316
+ cursor_x = self.strand_margin + (cursor_position * self.base_width)
317
+ cursor_y = (line_number * self.line_spacing) + (self.line_height * 0.1)
318
+ cursor_height = self.line_height * 2 + 5
319
+
320
+ self.sequence_cursor.set_position(cursor_x, cursor_y, cursor_height)
321
+ self.sequence_cursor.show()
322
+
323
+ self.current_cursor_pos = current_pos
324
+
325
+ except Exception as e:
326
+ print(f"Error highlighting: {str(e)}")
327
+
328
+ event.accept()
329
+
330
+ def mouseReleaseEvent(self, event):
331
+ """Handle mouse release to end selection"""
332
+ if self.selection_active:
333
+ self.selection_active = False
334
+
335
+ # Emit the final selection position
336
+ if hasattr(self, 'selection_start') and self.selection_start is not None:
337
+ self.insertion_point_selected.emit(self.selection_start)
338
+
339
+ event.accept()
340
+
341
+ def boundingRect(self):
342
+ """Return bounding rectangle for entire sequence area"""
343
+ if not hasattr(self, 'sequence_length') or self.sequence_length == 0:
344
+ return QRectF(0, 0, 1, 1) # Return minimal rect if no sequence
345
+
346
+ total_lines = (self.sequence_length + self.bases_per_line - 1) // self.bases_per_line
347
+ width = self.strand_margin * 2 + (self.bases_per_line * self.base_width)
348
+ height = total_lines * self.line_spacing
349
+
350
+ # Add padding
351
+ padding = 20
352
+ return QRectF(-padding, -padding, width + 2*padding, height + 2*padding)
353
+
354
+ def paint(self, painter, option, widget):
355
+ """Paint method required by QGraphicsObject"""
356
+ pass
src/views/dna_viewer/components/sequence_viewer.py ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import traceback
2
+ from PyQt6.QtWidgets import QGraphicsObject, QApplication, QGraphicsSimpleTextItem, QGraphicsLineItem
3
+ from PyQt6.QtCore import Qt, QRectF, pyqtSignal
4
+ from PyQt6.QtGui import QPen, QFont, QColor
5
+ from .nucleotide_item import NucleotideItem
6
+ import logging
7
+
8
+ class SequenceViewer(QGraphicsObject):
9
+ """Component for displaying and interacting with DNA sequence"""
10
+ sequence_selected = pyqtSignal(int, int) # Emit start and end positions
11
+ cursor_position_changed = pyqtSignal(int) # Emit cursor position
12
+
13
+ def __init__(self, parent=None, logger=None):
14
+ super().__init__(parent)
15
+
16
+ self.logger = logging.getLogger(__name__)
17
+
18
+ self._init_properties()
19
+ self._init_graphics_storage()
20
+
21
+ def _init_properties(self):
22
+ """Initialize properties"""
23
+ # Layout properties
24
+ self.strand_margin = 40
25
+ self.base_width = 15
26
+ self.bases_per_line = 70
27
+ self.line_height = 25
28
+ self.line_spacing = 80
29
+
30
+ # Sequence properties
31
+ self.sequence = ""
32
+ self.start_pos = 0
33
+
34
+ # Selection properties
35
+ self.selection_start = None
36
+ self.selection_end = None
37
+ self.drag_start_pos = None
38
+ self.selection_active = False
39
+
40
+ # Cursor properties
41
+ self.cursor_pos = None
42
+
43
+ # Clipboard
44
+ self.clipboard = QApplication.clipboard()
45
+
46
+ # Add tracking for cleared selection highlights
47
+ self.cleared_selection_positions = set() # Track positions where selection was cleared
48
+
49
+ def _init_graphics_storage(self):
50
+ """Initialize storage for graphics items"""
51
+ self.nucleotides = []
52
+ self.highlighted_regions = []
53
+ self.nucleotide_map = {'+': [], '-': []}
54
+ self.plot_lines = []
55
+ self.tick_lines = []
56
+
57
+ def set_data(self, sequence, start_pos=None):
58
+ """Set sequence data and create nucleotide items"""
59
+ try:
60
+ if not sequence:
61
+ self.logger.warning("Empty sequence provided")
62
+ return
63
+
64
+ self.sequence = sequence
65
+ self.start_pos = start_pos if start_pos is not None else 0
66
+
67
+ # Store current highlights, excluding cleared selection highlights
68
+ current_highlights = []
69
+ selection_blue = QColor(100, 150, 255, 100)
70
+
71
+ for nuc in self.nucleotides:
72
+ if nuc.is_highlighted:
73
+ idx = self.get_nucleotide_position(nuc)
74
+ pos = idx // 2 # Convert to sequence position
75
+
76
+ # Only store if it's not a selection highlight that was cleared
77
+ if nuc.highlight_color != selection_blue or pos not in self.cleared_selection_positions:
78
+ current_highlights.append({
79
+ 'position': pos,
80
+ 'color': nuc.highlight_color
81
+ })
82
+
83
+ # Batch update
84
+ if self.scene():
85
+ views = self.scene().views()
86
+ for view in views:
87
+ view.setUpdatesEnabled(False)
88
+
89
+ try:
90
+ # Create display
91
+ self._create_display()
92
+
93
+ # Reapply highlights, excluding cleared selections
94
+ for highlight in current_highlights:
95
+ pos = highlight['position']
96
+ color = highlight['color']
97
+
98
+ # Skip if this was a cleared selection
99
+ if color == selection_blue and pos in self.cleared_selection_positions:
100
+ continue
101
+
102
+ pos_strand_idx = pos * 2
103
+ neg_strand_idx = pos * 2 + 1
104
+
105
+ for idx in [pos_strand_idx, neg_strand_idx]:
106
+ if idx < len(self.nucleotides):
107
+ nuc = self.nucleotides[idx]
108
+ nuc.is_highlighted = True
109
+ nuc.highlight_color = color
110
+ nuc.update()
111
+
112
+ finally:
113
+ # Re-enable updates
114
+ if self.scene():
115
+ for view in views:
116
+ view.setUpdatesEnabled(True)
117
+ view.viewport().update()
118
+
119
+ except Exception as e:
120
+ self.logger.error(f"Error in set_data: {str(e)}")
121
+
122
+ def _create_display(self):
123
+ try:
124
+ # Clear existing items
125
+ self.cleanup_graphics()
126
+
127
+ total_lines = (len(self.sequence) + self.bases_per_line - 1) // self.bases_per_line
128
+
129
+ nucleotides_batch = []
130
+ plot_lines_batch = []
131
+ tick_lines_batch = []
132
+ position_numbers = []
133
+
134
+ current_pos = 0
135
+ while current_pos < len(self.sequence):
136
+ remaining_bases = len(self.sequence) - current_pos
137
+ bases_this_line = min(self.bases_per_line, remaining_bases)
138
+ line_text = self.sequence[current_pos:current_pos + bases_this_line]
139
+
140
+ line_num = current_pos // self.bases_per_line
141
+ y_pos = line_num * self.line_spacing
142
+
143
+ # Create nucleotides for this line
144
+ for i, nuc in enumerate(line_text):
145
+ x_pos = i * self.base_width + self.strand_margin
146
+
147
+ # Calculate absolute position in sequence
148
+ abs_pos = current_pos + i
149
+
150
+ # Determine if this nucleotide is part of padding
151
+ # Only consider it padding if it's at the start or end of the sequence
152
+ is_padding = (nuc.islower() and nuc in 'atgc' and
153
+ (abs_pos < 30 or abs_pos >= len(self.sequence) - 30))
154
+
155
+ # Create positive strand nucleotide
156
+ nuc_item = NucleotideItem(
157
+ nucleotide=nuc,
158
+ x=x_pos,
159
+ y=y_pos + self.line_height * 0.1,
160
+ width=self.base_width,
161
+ is_uppercase=nuc.isupper(),
162
+ is_padding=is_padding,
163
+ parent=self
164
+ )
165
+ nucleotides_batch.append(nuc_item)
166
+
167
+ # Create complement strand nucleotide
168
+ complement_item = NucleotideItem(
169
+ nucleotide=nuc,
170
+ x=x_pos,
171
+ y=y_pos + self.line_height * 1.45,
172
+ width=self.base_width,
173
+ is_uppercase=nuc.isupper(),
174
+ is_complement=True,
175
+ is_padding=is_padding,
176
+ parent=self
177
+ )
178
+ nucleotides_batch.append(complement_item)
179
+
180
+ # Create plot line
181
+ plot_y = y_pos + self.line_height * 1.15
182
+ plot_line = QGraphicsLineItem(
183
+ self.strand_margin,
184
+ plot_y,
185
+ self.strand_margin + bases_this_line * self.base_width,
186
+ plot_y,
187
+ self
188
+ )
189
+ plot_line.setPen(QPen(Qt.GlobalColor.black, 1))
190
+ plot_lines_batch.append(plot_line)
191
+
192
+ # Add position number at end of line
193
+ end_pos = str(self.start_pos + current_pos + bases_this_line)
194
+ pos_item = QGraphicsSimpleTextItem(end_pos, self)
195
+ pos_item.setFont(QFont("Courier", 12))
196
+
197
+ # Position text with fixed offset
198
+ extra_spacing = 25 if line_num == total_lines - 1 else 0
199
+ pos_x = self.strand_margin + (bases_this_line * self.base_width) + extra_spacing + 10
200
+ pos_y = plot_y - pos_item.boundingRect().height()/2
201
+ pos_item.setPos(pos_x, pos_y)
202
+ position_numbers.append(pos_item)
203
+
204
+ # Create tick marks
205
+ for i in range(bases_this_line):
206
+ x_pos = i * self.base_width + self.strand_margin
207
+ pos_1_based = current_pos + i + 1
208
+
209
+ tick_height = 12 if (i == 0 and current_pos == 0) or \
210
+ (i == bases_this_line - 1 and current_pos + bases_this_line == len(self.sequence)) else \
211
+ 10 if pos_1_based % 10 == 0 else \
212
+ 8 if pos_1_based % 5 == 0 else 5
213
+
214
+ tick = QGraphicsLineItem(
215
+ x_pos + self.base_width/2,
216
+ plot_y - tick_height/2,
217
+ x_pos + self.base_width/2,
218
+ plot_y + tick_height/2,
219
+ self
220
+ )
221
+ tick.setPen(QPen(Qt.GlobalColor.black, 1))
222
+ tick_lines_batch.append(tick)
223
+
224
+ current_pos += bases_this_line
225
+
226
+ # Add all items to scene in batches
227
+ self.nucleotides = nucleotides_batch
228
+ self.plot_lines = plot_lines_batch
229
+ self.tick_lines = tick_lines_batch
230
+
231
+ # Add strand indicators and position numbers
232
+ self._add_strand_indicators(total_lines)
233
+
234
+ except Exception as e:
235
+ self.logger.error(f"Error in create optimized display: {str(e)}")
236
+
237
+ def _add_strand_indicators(self, total_lines):
238
+ # Add first line indicators
239
+ five_prime_pos = QGraphicsSimpleTextItem("5'", self)
240
+ five_prime_pos.setFont(QFont("Arial", 10))
241
+ five_prime_pos.setPos(0, self.line_height * 0.26)
242
+
243
+ three_prime_neg = QGraphicsSimpleTextItem("3'", self)
244
+ three_prime_neg.setFont(QFont("Arial", 10))
245
+ three_prime_neg.setPos(0, self.line_height * 1.58)
246
+
247
+ # Add last line indicators
248
+ last_line_width = (len(self.sequence) % self.bases_per_line) * self.base_width
249
+ if last_line_width == 0:
250
+ last_line_width = self.bases_per_line * self.base_width
251
+
252
+ three_prime_pos = QGraphicsSimpleTextItem("3'", self)
253
+ three_prime_pos.setFont(QFont("Arial", 10))
254
+ three_prime_pos.setPos(
255
+ last_line_width + self.strand_margin + 20,
256
+ (total_lines - 1) * self.line_spacing + self.line_height * 0.26
257
+ )
258
+
259
+ five_prime_neg = QGraphicsSimpleTextItem("5'", self)
260
+ five_prime_neg.setFont(QFont("Arial", 10))
261
+ five_prime_neg.setPos(
262
+ last_line_width + self.strand_margin + 20,
263
+ (total_lines - 1) * self.line_spacing + self.line_height * 1.58
264
+ )
265
+
266
+ def cleanup_graphics(self):
267
+ """Clean up all graphics items"""
268
+ try:
269
+ # Remove plot lines
270
+ for line in self.plot_lines:
271
+ if line.scene():
272
+ line.scene().removeItem(line)
273
+ self.plot_lines.clear()
274
+
275
+ # Remove tick lines
276
+ for line in self.tick_lines:
277
+ if line.scene():
278
+ line.scene().removeItem(line)
279
+ self.tick_lines.clear()
280
+
281
+ # Remove nucleotides
282
+ for nuc in self.nucleotides:
283
+ if nuc.scene():
284
+ nuc.scene().removeItem(nuc)
285
+ self.nucleotides.clear()
286
+
287
+ # Remove all text items (including position numbers)
288
+ for item in self.childItems():
289
+ if isinstance(item, QGraphicsSimpleTextItem):
290
+ if item.scene():
291
+ item.scene().removeItem(item)
292
+
293
+ # Clear nucleotide maps but preserve highlights
294
+ self.nucleotide_map['+'].clear()
295
+ self.nucleotide_map['-'].clear()
296
+
297
+ self.logger.debug("Cleaned up all graphics items")
298
+
299
+ except Exception as e:
300
+ self.logger.error(f"Error in cleanup_graphics: {str(e)}")
301
+
302
+ def highlight_sequence(self, start_pos, end_pos, color, strand='+'):
303
+ """Highlight sequence with proper strand handling"""
304
+ try:
305
+ # Store highlight information
306
+ self.highlighted_regions.append((start_pos, end_pos, color, strand))
307
+
308
+ # Calculate which lines contain the sequence
309
+ start_line = start_pos // self.bases_per_line
310
+ end_line = end_pos // self.bases_per_line
311
+
312
+ # Calculate positions within lines
313
+ start_pos_in_line = start_pos % self.bases_per_line
314
+ end_pos_in_line = end_pos % self.bases_per_line
315
+
316
+ self.logger.debug(f"Highlighting from line {start_line} to {end_line}")
317
+ self.logger.debug(f"Start pos in line: {start_pos_in_line}, End pos in line: {end_pos_in_line}")
318
+
319
+ # For each line that contains part of the sequence
320
+ for line_num in range(start_line, end_line + 1):
321
+ # Calculate start and end positions for this line
322
+ line_start = start_pos_in_line if line_num == start_line else 0
323
+ line_end = end_pos_in_line if line_num == end_line else self.bases_per_line - 1
324
+
325
+ # Calculate base indices for this line
326
+ base_start = line_num * self.bases_per_line + line_start
327
+ base_end = line_num * self.bases_per_line + line_end
328
+
329
+ # Highlight nucleotides
330
+ for i in range(base_start, base_end + 1):
331
+ if i >= len(self.nucleotides):
332
+ break
333
+
334
+ # For negative strand, highlight both strands' nucleotides
335
+ if strand == '-':
336
+ # Highlight negative strand nucleotide
337
+ neg_idx = i * 2 + 1 # Odd indices for negative strand
338
+ if neg_idx < len(self.nucleotides):
339
+ nuc = self.nucleotides[neg_idx]
340
+ nuc.is_highlighted = True
341
+ nuc.highlight_color = color
342
+ nuc.update()
343
+ else:
344
+ # Highlight positive strand nucleotide
345
+ pos_idx = i * 2 # Even indices for positive strand
346
+ if pos_idx < len(self.nucleotides):
347
+ nuc = self.nucleotides[pos_idx]
348
+ nuc.is_highlighted = True
349
+ nuc.highlight_color = color
350
+ nuc.update()
351
+
352
+ # Force scene update
353
+ if self.scene():
354
+ self.scene().update()
355
+
356
+ self.logger.debug(f"Highlighted sequence on strand {strand} from {start_pos} to {end_pos}")
357
+
358
+ except Exception as e:
359
+ self.logger.error(f"Error in highlight_sequence: {str(e)}")
360
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
361
+
362
+ def clear_highlights(self):
363
+ """Clear all highlights with optimized rendering"""
364
+ try:
365
+ # Get view and disable updates
366
+ view = None
367
+ if self.scene():
368
+ views = self.scene().views()
369
+ if views:
370
+ view = views[0]
371
+ view.setUpdatesEnabled(False)
372
+
373
+ try:
374
+ self.highlighted_regions.clear()
375
+
376
+ # Batch collect nucleotides that need updating
377
+ nucleotides_to_update = []
378
+
379
+ for nuc in self.nucleotides:
380
+ if nuc.is_highlighted:
381
+ nuc.is_highlighted = False
382
+ nuc.highlight_color = None
383
+ nucleotides_to_update.append(nuc)
384
+
385
+ # Update all nucleotides in a single batch
386
+ if nucleotides_to_update:
387
+ # Use prepareGeometryChange for better performance
388
+ for nuc in nucleotides_to_update:
389
+ nuc.prepareGeometryChange()
390
+
391
+ # Force a single scene update
392
+ if self.scene():
393
+ self.scene().update()
394
+
395
+ finally:
396
+ # Re-enable view updates
397
+ if view:
398
+ view.setUpdatesEnabled(True)
399
+ view.viewport().update()
400
+
401
+ except Exception as e:
402
+ self.logger.error(f"Error in clear_highlights: {str(e)}")
403
+ # Make sure view updates are re-enabled
404
+ if self.scene():
405
+ views = self.scene().views()
406
+ if views:
407
+ views[0].setUpdatesEnabled(True)
408
+ views[0].viewport().update()
409
+
410
+ def get_nucleotide_position(self, nucleotide):
411
+ """Get the position of a nucleotide in the sequence"""
412
+ try:
413
+ idx = self.nucleotides.index(nucleotide)
414
+ return idx
415
+ except ValueError:
416
+ return -1
417
+
418
+ def boundingRect(self):
419
+ """Return the bounding rectangle"""
420
+ if not self.sequence:
421
+ return QRectF(0, 0, 100, 100) # Return a default size when empty
422
+
423
+ # Calculate total width including margins
424
+ width = (self.base_width * self.bases_per_line) + (self.strand_margin * 2)
425
+
426
+ # Calculate height using line spacing
427
+ total_lines = (len(self.sequence) + self.bases_per_line - 1) // self.bases_per_line
428
+ height = total_lines * self.line_spacing
429
+
430
+ # Add some padding
431
+ height += 50
432
+ width += 50
433
+
434
+ return QRectF(0, 0, width, height)
435
+
436
+ def paint(self, painter, option, widget):
437
+ """Paint method required by QGraphicsObject"""
438
+ pass
src/views/dna_viewer/dna_feature_viewer.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QGraphicsView, QGraphicsScene,
2
+ QLabel, QFrame)
3
+ from PyQt6.QtCore import Qt, pyqtSignal
4
+ from PyQt6.QtGui import QBrush, QColor
5
+ from .components.sequence_viewer import SequenceViewer
6
+ from .components.feature_viewer import FeatureViewer
7
+ from .components.ruler import Ruler
8
+ from .components.sequence_insertion_zone import SequenceInsertionZone
9
+ import logging
10
+ import traceback
11
+
12
+ class DNAFeatureViewer(QWidget):
13
+ """Main widget for displaying DNA sequences"""
14
+ sequence_selected = pyqtSignal(int, int) # Emit start and end positions
15
+
16
+ def __init__(self, parent=None):
17
+ super().__init__(parent)
18
+
19
+ # Get logger from parent or global settings
20
+ if parent and hasattr(parent, 'logger'):
21
+ self.logger = parent.logger
22
+ else:
23
+ self.logger = logging.getLogger(__name__)
24
+
25
+ # Create layout
26
+ self.layout = QVBoxLayout(self)
27
+ self.layout.setContentsMargins(0, 0, 0, 0)
28
+ self.layout.setSpacing(0)
29
+
30
+ # Initialize components
31
+ self._init_views()
32
+ self._init_components()
33
+ self._init_status_panel()
34
+ self._init_connections()
35
+
36
+ # Set focus policy for the widget itself
37
+ self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
38
+ self.setFocus()
39
+
40
+ def _init_views(self):
41
+ """Initialize graphics views"""
42
+ # Create ruler view
43
+ self.ruler_view = QGraphicsView()
44
+ self.ruler_scene = Ruler()
45
+ self.ruler_view.setScene(self.ruler_scene)
46
+ self.ruler_view.setFixedHeight(25)
47
+ self.ruler_view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
48
+ self.ruler_view.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
49
+ self.ruler_view.setViewportMargins(0, 0, 0, 0)
50
+ self.ruler_view.setFrameStyle(0)
51
+ self.ruler_view.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
52
+ self.ruler_view.setEnabled(False) # Disable all user interaction with ruler
53
+
54
+ # Set background color for both view and viewport
55
+ background_color = QColor(240, 240, 240)
56
+ self.ruler_view.setBackgroundBrush(QBrush(background_color))
57
+ self.ruler_view.viewport().setStyleSheet(f"background-color: rgb({background_color.red()}, {background_color.green()}, {background_color.blue()})")
58
+ self.ruler_view.setAutoFillBackground(True)
59
+
60
+ # Create main view with proper event handling
61
+ self.view = QGraphicsView()
62
+ self.scene = QGraphicsScene(self)
63
+ self.view.setScene(self.scene)
64
+
65
+ # Add these lines to remove the frame from main view
66
+ self.view.setFrameStyle(0) # Remove frame
67
+ self.view.setViewportMargins(0, 0, 0, 0) # Remove margins
68
+
69
+ # Set alignment to force left anchoring
70
+ self.view.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
71
+
72
+ # Make sure scene events are handled
73
+ self.scene.setItemIndexMethod(QGraphicsScene.ItemIndexMethod.NoIndex)
74
+
75
+ def _init_components(self):
76
+ try:
77
+ # Create sequence and feature viewers
78
+ if not hasattr(self, 'sequence_viewer'):
79
+ self.sequence_viewer = SequenceViewer(logger=self.logger)
80
+ # Reduce left margin/padding
81
+ self.sequence_viewer.strand_margin = 20
82
+ self.scene.addItem(self.sequence_viewer)
83
+
84
+ if not hasattr(self, 'feature_viewer'):
85
+ # Create feature viewer and add to scene
86
+ self.feature_viewer = FeatureViewer()
87
+ # Match margin with sequence viewer
88
+ self.feature_viewer.strand_margin = 20
89
+ self.scene.addItem(self.feature_viewer)
90
+
91
+ # Add sequence insertion zone
92
+ if not hasattr(self, 'insertion_zone'):
93
+ self.insertion_zone = SequenceInsertionZone()
94
+ # Match margin with sequence viewer
95
+ self.insertion_zone.strand_margin = 20
96
+ self.scene.addItem(self.insertion_zone)
97
+
98
+ # Add views to layout
99
+ self.layout.insertWidget(0, self.ruler_view)
100
+ self.layout.addWidget(self.view)
101
+
102
+ except Exception as e:
103
+ self.logger.error(f"Error in _init_components: {str(e)}")
104
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
105
+
106
+ def _init_status_panel(self):
107
+ """Initialize status panel"""
108
+ self.status_panel = QLabel()
109
+ self.status_panel.setFrameStyle(QFrame.Shape.Panel | QFrame.Shadow.Sunken)
110
+ self.status_panel.setStyleSheet("""
111
+ QLabel {
112
+ background-color: #f0f0f0;
113
+ padding: 5px;
114
+ border-top: 1px solid #ccc;
115
+ min-height: 20px;
116
+ }
117
+ """)
118
+ self.layout.addWidget(self.status_panel)
119
+
120
+ def _init_connections(self):
121
+ """Initialize signal connections"""
122
+ # Connect sequence viewer signals
123
+ self.sequence_viewer.sequence_selected.connect(self._on_sequence_selected)
124
+ self.sequence_viewer.cursor_position_changed.connect(self._on_cursor_position_changed)
125
+
126
+ # Connect feature viewer signals
127
+ self.feature_viewer.cursor_position_changed.connect(self._on_cursor_position_changed)
128
+
129
+ # Add viewport resize handling
130
+ self.view.viewport().installEventFilter(self)
131
+
132
+ def set_data(self, sequence, features=None, start_pos=None):
133
+ """Set sequence and feature data"""
134
+ try:
135
+ if start_pos is None:
136
+ start_pos = 0
137
+
138
+ # self.logger.debug(f"Features: {features[:2] if features else None}")
139
+
140
+ # Store original start position for status panel
141
+ self._original_start_pos = start_pos
142
+
143
+ # Update components
144
+ self.sequence_viewer.set_data(sequence, start_pos)
145
+ if features is not None:
146
+ self.feature_viewer.set_data(sequence, features, start_pos)
147
+
148
+ # Update insertion zone
149
+ self.insertion_zone.create_zones(
150
+ sequence_length=len(sequence),
151
+ base_width=self.sequence_viewer.base_width,
152
+ strand_margin=self.sequence_viewer.strand_margin,
153
+ line_height=self.sequence_viewer.line_height,
154
+ bases_per_line=self.sequence_viewer.bases_per_line,
155
+ line_spacing=self.sequence_viewer.line_spacing
156
+ )
157
+
158
+ # Position components
159
+ self.feature_viewer.setY(0)
160
+ self.insertion_zone.setY(0)
161
+
162
+ # Update scene rect
163
+ combined_rect = self.sequence_viewer.boundingRect().united(
164
+ self.feature_viewer.boundingRect()
165
+ ).united(
166
+ self.insertion_zone.boundingRect()
167
+ )
168
+ self.scene.setSceneRect(combined_rect)
169
+
170
+ # Update ruler
171
+ self.ruler_scene.create_ruler(self.sequence_viewer.bases_per_line)
172
+
173
+ # Update status panel with original gene positions
174
+ sequence_length = len(sequence) if sequence else 0
175
+ self.status_panel.setText(
176
+ f"Showing: {self._original_start_pos}...{self._original_start_pos + sequence_length} = {sequence_length} bp"
177
+ )
178
+
179
+ self.update()
180
+ self.logger.debug("Finished setting data")
181
+
182
+ except Exception as e:
183
+ self.logger.error(f"Error setting data: {str(e)}")
184
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
185
+
186
+ def _on_sequence_selected(self, start_pos, end_pos):
187
+ """Handle sequence selection"""
188
+ try:
189
+ # Calculate selected length
190
+ selected_length = end_pos - start_pos + 1
191
+
192
+ # Update only the status panel
193
+ self.status_panel.setText(f"Selected: {start_pos}...{end_pos} = {selected_length} bp")
194
+
195
+ # Emit signal without updating line edits
196
+ self.sequence_selected.emit(start_pos, end_pos)
197
+
198
+ except Exception as e:
199
+ self.logger.error(f"Error handling sequence selection: {str(e)}")
200
+
201
+ def _on_cursor_position_changed(self, position):
202
+ """Handle cursor position changes"""
203
+ try:
204
+ if position >= 0:
205
+ # Convert position to be relative to gene's actual start position
206
+ absolute_position = self._original_start_pos + position
207
+ self.status_panel.setText(f"Insertion Point: {absolute_position}")
208
+ self.logger.debug(f"Cursor position changed to absolute position: {absolute_position}")
209
+ else:
210
+ # Reset to showing current sequence range
211
+ if hasattr(self, 'sequence_viewer'):
212
+ sequence = self.sequence_viewer.sequence
213
+ sequence_length = len(sequence)
214
+ self.status_panel.setText(
215
+ f"Showing: {self._original_start_pos}...{self._original_start_pos + sequence_length} = {sequence_length} bp"
216
+ )
217
+ except Exception as e:
218
+ self.logger.error(f"Error handling cursor position change: {str(e)}")
219
+
220
+ def eventFilter(self, obj, event):
221
+ """Handle viewport resize events"""
222
+ if obj == self.view.viewport() and event.type() == event.Type.Resize:
223
+ try:
224
+ viewport_width = event.size().width()
225
+ margin = 100
226
+
227
+ available_width = viewport_width - margin
228
+ base_width = self.sequence_viewer.base_width
229
+
230
+ max_bases = (available_width // base_width)
231
+ new_bases = (max_bases // 10) * 10
232
+ new_bases = max(10, min(new_bases, 200))
233
+
234
+ if new_bases != self.sequence_viewer.bases_per_line:
235
+ # Store current sequence and features
236
+ current_sequence = self.sequence_viewer.sequence
237
+ current_start = self.sequence_viewer.start_pos
238
+
239
+ # Store only guide highlights (red/green), not selection highlights (blue)
240
+ current_highlights = []
241
+ selection_blue = QColor(100, 150, 255, 100)
242
+
243
+ for nuc in self.sequence_viewer.nucleotides:
244
+ if nuc.is_highlighted and nuc.highlight_color != selection_blue:
245
+ idx = self.sequence_viewer.get_nucleotide_position(nuc)
246
+ current_highlights.append({
247
+ 'position': idx // 2,
248
+ 'color': nuc.highlight_color
249
+ })
250
+
251
+ # Store cursor position
252
+ cursor_sequence_pos = None
253
+ if hasattr(self.insertion_zone, 'current_cursor_pos'):
254
+ cursor_sequence_pos = self.insertion_zone.current_cursor_pos
255
+
256
+ try:
257
+ # Update bases per line
258
+ self.sequence_viewer.bases_per_line = new_bases
259
+ self.feature_viewer.bases_per_line = new_bases
260
+
261
+ # Update components
262
+ self.set_data(current_sequence, None, current_start)
263
+
264
+ # Reapply only guide highlights
265
+ for highlight in current_highlights:
266
+ pos = highlight['position']
267
+ pos_strand_idx = pos * 2
268
+ neg_strand_idx = pos * 2 + 1
269
+
270
+ for idx in [pos_strand_idx, neg_strand_idx]:
271
+ if idx < len(self.sequence_viewer.nucleotides):
272
+ nuc = self.sequence_viewer.nucleotides[idx]
273
+ nuc.is_highlighted = True
274
+ nuc.highlight_color = highlight['color']
275
+ nuc.update()
276
+
277
+ # Restore cursor position
278
+ if cursor_sequence_pos is not None:
279
+ # Calculate new visual position based on sequence position
280
+ line_number = cursor_sequence_pos // new_bases
281
+ pos_in_line = cursor_sequence_pos % new_bases
282
+
283
+ # Calculate exact pixel coordinates for cursor
284
+ cursor_x = self.sequence_viewer.strand_margin + (pos_in_line * self.sequence_viewer.base_width)
285
+ cursor_y = (line_number * self.sequence_viewer.line_spacing) + (self.sequence_viewer.line_height * 0.1)
286
+ cursor_height = self.sequence_viewer.line_height * 2 + 5
287
+
288
+ # Update cursor position
289
+ if hasattr(self.insertion_zone, 'sequence_cursor'):
290
+ self.insertion_zone.sequence_cursor.set_position(cursor_x, cursor_y, cursor_height)
291
+ self.insertion_zone.sequence_cursor.show()
292
+ self.insertion_zone.current_cursor_pos = cursor_sequence_pos
293
+
294
+ # Update ruler
295
+ self.ruler_scene.create_ruler(new_bases)
296
+
297
+ # Update scroll positions
298
+ scroll_value = self.view.horizontalScrollBar().value()
299
+ self.ruler_view.horizontalScrollBar().setValue(scroll_value)
300
+
301
+ finally:
302
+ # Force immediate update
303
+ self.view.viewport().update()
304
+ self.ruler_view.viewport().update()
305
+
306
+ except Exception as e:
307
+ self.logger.error(f"Error handling resize event: {str(e)}")
308
+
309
+ return super().eventFilter(obj, event)